diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs index 49e53979a..005838b51 100644 --- a/Projects/Server/Serialization/GenericEntityPersistence.cs +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -255,7 +255,7 @@ public class GenericEntityPersistence : Persistence, IGenericEntityPersistenc try { using var op = new StreamWriter("world-save-errors.log", true); - op.WriteLine("{0}\t{1}", DateTime.UtcNow, message); + op.WriteLine("{0}\t{1}", Core.Now, message); op.WriteLine(new StackTrace(2).ToString()); op.WriteLine(); } diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs index 31f5c4a7e..815b9f54c 100644 --- a/Projects/Server/Serialization/IGenericReader.cs +++ b/Projects/Server/Serialization/IGenericReader.cs @@ -51,7 +51,7 @@ public interface IGenericReader { long.MinValue => DateTime.MinValue, long.MaxValue => DateTime.MaxValue, - var delta => new DateTime(delta + DateTime.UtcNow.Ticks, DateTimeKind.Utc) + var delta => new DateTime(delta + Core.Now.Ticks, DateTimeKind.Utc) }; } decimal ReadDecimal() => new(stackalloc int[4] { ReadInt(), ReadInt(), ReadInt(), ReadInt() }); diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs index 6595cb0fa..d512996da 100644 --- a/Projects/Server/Serialization/IGenericWriter.cs +++ b/Projects/Server/Serialization/IGenericWriter.cs @@ -70,7 +70,7 @@ public interface IGenericWriter } // Technically supports negative deltas for times in the past - Write(value.Ticks - DateTime.UtcNow.Ticks); + Write(value.Ticks - Core.Now.Ticks); } void Write(IPAddress value) { diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs index 8564f0bd0..25e9fe16b 100644 --- a/Projects/Server/World/EntityPersistence.cs +++ b/Projects/Server/World/EntityPersistence.cs @@ -119,7 +119,7 @@ public static class EntityPersistence return map; } - var now = DateTime.UtcNow; + var now = Core.Now; for (int i = 0; i < count; ++i) { diff --git a/Projects/UOContent/Engines/XMLSpawner/BaseXmlSpawner.cs b/Projects/UOContent/Engines/XMLSpawner/BaseXmlSpawner.cs new file mode 100644 index 000000000..fca83853d --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/BaseXmlSpawner.cs @@ -0,0 +1,3956 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +using Server.Commands; +using Server.Items; + +namespace Server.Mobiles; + +public delegate void XmlGumpCallback(Mobile from, object invoker, string response); + +public class BaseXmlSpawner +{ + + [Flags] + public enum KeywordFlags + { + HoldSpawn = 0x01, + HoldSequence = 0x02, + Serialize = 0x04, + Defrag = 0x08 + } + + public class TypeInfo + { + public List plist = new(); // hold propertyinfo list + public Type t; + } + + private static readonly Type typeofTimeSpan = typeof(TimeSpan); + private static readonly Type typeofCustomEnum = typeof(CustomEnumAttribute); + + private static bool IsParsable(Type t) + { + return t == typeofTimeSpan || t.GetMethod("Parse", m_ParseTypes) != null; + } + + private static readonly Type[] m_ParseTypes = { typeof(string) }; + private static readonly object[] m_ParseParams = new object[1]; + + private static object Parse(object o, Type t, string value) + { + var method = t.GetMethod("Parse", m_ParseTypes); + + m_ParseParams[0] = value; + + return method.Invoke(o, m_ParseParams); + } + + private static readonly Type[] m_NumericTypes = + { + typeof(byte), typeof(sbyte), + typeof(short), typeof(ushort), + typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(Serial) + }; + + public static bool IsNumeric(Type t) + { + return Array.IndexOf(m_NumericTypes, t) >= 0; + } + + private static readonly Type typeofType = typeof(Type); + + private static bool IsType(Type t) + { + return t == typeofType; + } + + private static readonly Type typeofChar = typeof(char); + + private static bool IsChar(Type t) + { + return t == typeofChar; + } + + private static readonly Type typeofString = typeof(string); + + private static bool IsString(Type t) + { + return t == typeofString; + } + + private static bool IsEnum(Type t) + { + return t.IsEnum; + } + + private static bool IsCustomEnum(Type t) + { + return t.IsDefined(typeofCustomEnum, false); + } + + private enum TypeKeyword + { + SET, + GOTO, + COMMAND, + SPAWN, + DESPAWN + } + + private enum ValueKeyword + { + PLAYERSINRANGE, + RANDNAME + } + + private enum ValuemodKeyword + { + INC, + MOB, + TRIGMOB, + PLAYERSINRANGE + } + + // name of mobile used to issue commands via the COMMAND keyword. The accesslevel of the mobile will determine + // the accesslevel of commands that can be issued. + // if this is null, then COMMANDS can only be issued when triggered by players of the appropriate accesslevel + private static readonly string CommandMobileName = null; + + private static readonly Dictionary typeKeywordHash = new(); + private static readonly Dictionary valueKeywordHash = new(); + private static readonly Dictionary valuemodKeywordHash = new(); + + private static readonly char[] slashdelim = { '/' }; + private static readonly char[] commadelim = { ',' }; + private static readonly char[] semicolondelim = { ';' }; + private static readonly char[] literalend = { 'ยง' }; + + public static bool IsValueKeyword(string str) + { + if (string.IsNullOrEmpty(str) || !char.IsUpper(str[0])) + { + return false; + } + + return valueKeywordHash.ContainsKey(str); + } + + public static bool IsValuemodKeyword(string str) + { + if (string.IsNullOrEmpty(str) || !char.IsUpper(str[0])) + { + return false; + } + + return valuemodKeywordHash.ContainsKey(str); + } + + public static bool IsTypeKeyword(string typeName) + { + if (string.IsNullOrEmpty(typeName) || !char.IsUpper(typeName[0])) + { + return false; + } + + return typeKeywordHash.ContainsKey(typeName); + } + + public static bool IsTypeOrItemKeyword(string typeName) + { + if (string.IsNullOrEmpty(typeName) || !char.IsUpper(typeName[0])) + { + return false; + } + + return typeKeywordHash.ContainsKey(typeName); + } + + public static void RemoveKeyword(string name) + { + if (name == null) + { + return; + } + + name = name.Trim().ToUpper(); + + _ = typeKeywordHash.Remove(name); + + _ = valueKeywordHash.Remove(name); + + _ = valuemodKeywordHash.Remove(name); + } + + public class KeywordTag + { + public KeywordFlags Flags; + public int Type; + private Timer m_Timer; + public DateTime m_End; + public DateTime m_TimeoutEnd; + public TimeSpan m_Delay; + public TimeSpan m_Timeout; + private XmlSpawner m_Spawner; + public string m_Condition; + public int m_Goto; + public bool Deleted; + public int Serial = -1; + public Mobile m_TrigMob; + public string Typename; + + public KeywordTag(string typename, XmlSpawner spawner) + : this(typename, spawner, -1) + { + } + + public KeywordTag(string typename, XmlSpawner spawner, int type) + : this(typename, spawner, type, TimeSpan.Zero, TimeSpan.Zero, null, -1) + { + } + + public KeywordTag(string typename, XmlSpawner spawner, int type, TimeSpan delay, TimeSpan timeout, string condition, int gotogroup) + { + Type = type; + m_Delay = delay; + m_Timeout = timeout; + m_TimeoutEnd = Core.Now + timeout; + m_Spawner = spawner; + m_Condition = condition; + m_Goto = gotogroup; + + Typename = typename; + // add the tag to the list + if (spawner != null && !spawner.Deleted) + { + m_TrigMob = spawner.TriggerMob; + spawner.m_KeywordTagList ??= new List(); + // calculate the serial index of the new tag by adding one to the last one if there is one, otherwise just reset to 0 + if (spawner.m_KeywordTagList.Count > 0) + { + Serial = spawner.m_KeywordTagList[spawner.m_KeywordTagList.Count - 1].Serial + 1; + } + else + { + Serial = 0; + } + + spawner.m_KeywordTagList.Add(this); + + switch (type) + { + case 0: // WAIT timer type + { + // start up the timer + DoTimer(delay, m_Delay, condition, gotogroup); + Flags |= KeywordFlags.HoldSpawn; + Flags |= KeywordFlags.Serialize; + + break; + } + case 1: // GUMP type + { + break; + } + + case 2: // GOTO type + { + Flags |= KeywordFlags.HoldSequence; + Flags |= KeywordFlags.Serialize; + + break; + } + default: + { + // dont do anything for other types + Flags |= KeywordFlags.Defrag; + break; + } + } + } + } + + public void Delete() + { + // and stop all timers + if (m_Timer != null && Type == 0) + { + m_Timer.Stop(); + } + + Deleted = true; + + // and remove it from the list + RemoveFromTagList(m_Spawner, this); + + } + + private void DoTimer(TimeSpan delay, TimeSpan repeatdelay, string condition, int gotogroup) + { + m_End = Core.Now + delay; + + m_Timer?.Stop(); + + m_Timer = new KeywordTimer(m_Spawner, this, delay, repeatdelay, condition, gotogroup); + _ = m_Timer.Start(); + } + + public void Serialize(IGenericWriter writer) + { + writer.Write(1); // version + // Version 1 + writer.Write((int)Flags); + // Version 0 + writer.Write(m_Spawner); + writer.Write(Type); + writer.Write(Serial); + if (Type == 0) + { + // save any timer information + writer.Write(m_End - Core.Now); + writer.Write(m_Delay); + writer.Write(m_Condition); + writer.Write(m_Goto); + writer.Write(m_TimeoutEnd - Core.Now); + writer.Write(m_Timeout); + writer.Write(m_TrigMob); + } + } + public void Deserialize(IGenericReader reader) + { + + var version = reader.ReadInt(); + switch (version) + { + case 1: + { + Flags = (KeywordFlags)reader.ReadInt(); + goto case 0; + } + case 0: + { + m_Spawner = reader.ReadEntity(); + Type = reader.ReadInt(); + Serial = reader.ReadInt(); + if (Type == 0) + { + // get any timer info + var delay = reader.ReadTimeSpan(); + m_Delay = reader.ReadTimeSpan(); + m_Condition = reader.ReadString(); + m_Goto = reader.ReadInt(); + + var timeoutdelay = reader.ReadTimeSpan(); + m_TimeoutEnd = Core.Now + timeoutdelay; + m_Timeout = reader.ReadTimeSpan(); + m_TrigMob = reader.ReadEntity(); + + DoTimer(delay, m_Delay, m_Condition, m_Goto); + } + break; + } + } + } + + // added the timer that begins on spawning tmp keywords + private class KeywordTimer : Timer + { + private readonly KeywordTag m_Tag; + private readonly XmlSpawner m_Spawner; + private readonly string m_Condition; + private readonly int m_Goto; + private readonly TimeSpan m_Repeatdelay; + + public KeywordTimer(XmlSpawner spawner, KeywordTag tag, TimeSpan delay, TimeSpan repeatdelay, string condition, int gotogroup) + : base(delay) + { + m_Tag = tag; + m_Spawner = spawner; + m_Condition = condition; + m_Goto = gotogroup; + m_Repeatdelay = repeatdelay; + } + + protected override void OnTick() + { + // if a condition is available then test it + if (!string.IsNullOrEmpty(m_Condition) && m_Spawner != null && m_Spawner.Running) + { + // if the test is valid then terminate the timer + + if (TestItemProperty(m_Spawner, m_Spawner, m_Condition, out _)) + { + // spawn the designated subgroup if specified + if (m_Goto >= 0 && m_Spawner != null && !m_Spawner.Deleted) + { + // set the trigmob to the mob that originally triggered the wait keyword + if (m_Tag != null) + { + m_Spawner.TriggerMob = m_Tag.m_TrigMob; + } + + // spawn the subgroup + _ = m_Spawner.SpawnSubGroup(m_Goto, 0); + } + + // get rid of the temporary tag + if (m_Tag != null && !m_Tag.Deleted) + { + m_Tag.Delete(); + } + + } + else + { + // otherwise restart it and keep on holding + if (m_Tag != null && !m_Tag.Deleted) + { + // check the timeout if applicable + if (m_Tag.m_Timeout > TimeSpan.Zero && m_Tag.m_TimeoutEnd < Core.Now) + { + // release the hold on spawning and delete the tag + m_Tag.Delete(); + } + else + { + m_Tag.DoTimer(m_Repeatdelay, m_Repeatdelay, m_Condition, m_Goto); + } + } + } + } + else + { + // and terminate the timer + if (m_Tag != null && !m_Tag.Deleted) + { + m_Tag.Delete(); + } + } + } + } + } + + public static string TagInfo(KeywordTag tag) + { + if (tag != null) + { + return $"{tag.Typename} : type={tag.Type} cond={tag.m_Condition} go={tag.m_Goto} del={tag.m_Delay} end={tag.m_End}"; + } + + return null; + } + + public static void RemoveFromTagList(XmlSpawner spawner, KeywordTag tag) + { + for (var i = 0; i < spawner.m_KeywordTagList.Count; i++) + { + if (tag == spawner.m_KeywordTagList[i]) + { + spawner.m_KeywordTagList.RemoveAt(i); + break; + } + } + } + + public static KeywordTag GetFromTagList(XmlSpawner spawner, int serial) + { + for (var i = 0; i < spawner.m_KeywordTagList.Count; i++) + { + if (serial == spawner.m_KeywordTagList[i].Serial) + { + return spawner.m_KeywordTagList[i]; + } + } + return null; + } + + private static string InternalGetValue(object o, PropertyInfo p, int index) + { + var type = p.PropertyType; + object value = null; + + if (type.IsPrimitive) + { + value = p.GetValue(o, null); + } + else if (type.GetInterface("IList") != null && index >= 0) + { + try + { + var arrayvalue = p.GetValue(o, null); + value = ((IList)arrayvalue)[index]; + } + catch { } + } + else + { + value = p.GetValue(o, null); + } + + string toString; + + if (value == null) + { + toString = "(-null-)"; + } + else if (IsNumeric(type)) + { + toString = string.Format("{0} (0x{0:X})", value); + } + else if (IsChar(type)) + { + toString = string.Format("'{0}' ({1} [0x{1:X}])", value, (int)value); + } + else if (IsString(type)) + { + toString = $"\"{value}\""; + } + else + { + toString = value.ToString(); + } + + return $"{p.Name} = {toString}"; + } + + public static bool IsItem(Type type) + { + return type != null && (type == typeof(Item) || type.IsSubclassOf(typeof(Item))); + } + + public static bool IsMobile(Type type) + { + return type != null && (type == typeof(Mobile) || type.IsSubclassOf(typeof(Mobile))); + } + + public static string ConstructFromString(PropertyInfo p, Type type, object obj, string value, ref object constructed) + { + object toSet; + + if (value == "(-null-)" && !type.IsValueType) + { + value = null; + } + + if (IsEnum(type)) + { + try + { + toSet = Enum.Parse(type, value, true); + } + catch + { + return "That is not a valid enumeration member."; + } + } + else if (IsCustomEnum(type)) + { + try + { + var info = p.PropertyType.GetMethod("Parse", new[] { typeof(string) }); + if (info != null) + { + toSet = info.Invoke(null, new object[] { value }); + } + else if (p.PropertyType == typeof(Enum) || p.PropertyType.IsSubclassOf(typeof(Enum))) + { + toSet = Enum.Parse(p.PropertyType, value, false); + } + else + { + toSet = null; + } + + if (toSet == null) + { + return "That is not a valid custom enumeration member."; + } + } + catch + { + return "That is not a valid custom enumeration member."; + } + } + else if (IsType(type)) + { + try + { + toSet = AssemblyHandler.FindTypeByName(value); + + if (toSet == null) + { + return "No type with that name was found."; + } + } + catch + { + return "No type with that name was found."; + } + } + else if (IsParsable(type)) + { + try + { + toSet = Parse(obj, type, value); + } + catch + { + return "That is not properly formatted."; + } + } + else if (value == null) + { + toSet = null; + } + else if (value.StartsWith("0x") && IsNumeric(type)) + { + try + { + toSet = Convert.ChangeType(Convert.ToUInt64(value.Substring(2), 16), type); + } + catch + { + return "That is not properly formatted. not convertible."; + } + } + else if (value.StartsWith("0x") && (IsItem(type) || IsMobile(type))) + { + try + { + // parse out the mobile or item name from the value string + var ispace = value.IndexOf(' '); + var valstr = value.Substring(2); + if (ispace > 0) + { + valstr = value.Substring(2, ispace - 2); + } + + toSet = World.FindEntity((Serial)Convert.ToUInt32(valstr, 16)); + // now check to make sure the object returned is consistent with the type + if (!(toSet is Mobile && IsMobile(type) || toSet is Item && IsItem(type))) + { + return "Item/Mobile type mismatch. cannot assign."; + } + } + catch + { + return "That is not properly formatted. not convertible."; + } + } + else if (type.GetInterface("IList") != null) + { + try + { + + var arrayvalue = p.GetValue(obj, null); + + var po = ((IList)arrayvalue)[0]; + + var atype = po.GetType(); + + toSet = Parse(obj, atype, value); + } + catch + { + return "That is not properly formatted."; + } + } + else + { + try + { + toSet = Convert.ChangeType(value, type); + } + catch + { + return "That is not properly formatted."; + } + } + + constructed = toSet; + + return null; + } + + public static string InternalSetValue(Mobile from, object o, PropertyInfo p, string value, bool shouldLog, int index) + { + object toSet = null; + var ptype = p.PropertyType; + + var result = ConstructFromString(p, p.PropertyType, o, value, ref toSet); + + if (result != null) + { + return result; + } + + try + { + if (shouldLog) + { + CommandLogging.LogChangeProperty(from, o, p.Name, value); + } + + if (ptype.IsPrimitive) + { + p.SetValue(o, toSet, null); + } + else if (ptype.GetInterface("IList") != null && index >= 0) + { + try + { + var arrayvalue = p.GetValue(o, null); + ((IList)arrayvalue)[index] = toSet; + } + catch { } + } + else + { + p.SetValue(o, toSet, null); + } + + return "Property has been set."; + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return "An exception was caught, the property may not be set."; + } + } + + // set property values with support for nested attributes + public static string SetPropertyValue(XmlSpawner spawner, object o, string name, string value) + { + if (o == null) + { + return "Null object"; + } + + var type = o.GetType(); + + var props = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); + + // parse the strings of the form property.attribute into two parts + // first get the property + var arglist = ParseString(name, 2, "."); + + var propname = arglist[0]; + + // do a bit of parsing to handle array references + var arraystring = propname.Split('['); + var index = 0; + if (arraystring.Length > 1) + { + // parse the property name from the indexing + propname = arraystring[0]; + + // then parse to get the index value + var arrayvalue = arraystring[1].Split(']'); + + if (arrayvalue.Length > 0) + { + _ = int.TryParse(arraystring[0], out index); + } + } + + if (arglist.Length == 2) + { + var plookup = LookupPropertyInfo(spawner, type, propname); + + object po; + if (plookup != null) + { + po = plookup.GetValue(o, null); + + // now set the nested attribute using the new property list + return SetPropertyValue(spawner, po, arglist[1], value); + } + + // is a nested property with attributes so first get the property + foreach (var p in props) + { + if (p.Name.InsensitiveEquals(propname)) + { + po = p.GetValue(o, null); + + // now set the nested attribute using the new property list + return SetPropertyValue(spawner, po, arglist[1], value); + } + } + } + else + { + // its just a simple single property + + var plookup = LookupPropertyInfo(spawner, type, propname); + + if (plookup != null) + { + if (!plookup.CanWrite) + { + return "Property is read only."; + } + + var returnvalue = InternalSetValue(null, o, plookup, value, false, index); + + return returnvalue; + } + // note, looping through all of the props turns out to be a significant performance bottleneck + // good place for optimization + + foreach (var p in props) + { + if (p.Name.InsensitiveEquals(propname)) + { + if (!p.CanWrite) + { + return "Property is read only."; + } + + var returnvalue = InternalSetValue(null, o, p, value, false, index); + + return returnvalue; + + } + } + } + + return "Property not found."; + } + + public static string SetPropertyObject(XmlSpawner spawner, object o, string name, object value) + { + if (o == null) + { + return "Null object"; + } + + var type = o.GetType(); + + var props = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); + + // parse the strings of the form property.attribute into two parts + // first get the property + var arglist = ParseString(name, 2, "."); + + if (arglist.Length == 2) + { + // is a nested property with attributes so first get the property + + // use the lookup table for optimization if possible + var plookup = LookupPropertyInfo(spawner, type, arglist[0]); + + object po; + if (plookup != null) + { + po = plookup.GetValue(o, null); + + // now set the nested attribute using the new property list + return SetPropertyObject(spawner, po, arglist[1], value); + } + + foreach (var p in props) + { + if (p.Name.InsensitiveEquals(arglist[0])) + { + po = p.GetValue(o, null); + + // now set the nested attribute using the new property list + return SetPropertyObject(spawner, po, arglist[1], value); + + } + } + } + else + { + // its just a simple single property + + // use the lookup table for optimization if possible + var plookup = LookupPropertyInfo(spawner, type, name); + + if (plookup != null) + { + if (!plookup.CanWrite) + { + return "Property is read only."; + } + + if (plookup.PropertyType == typeof(Mobile)) + { + plookup.SetValue(o, value, null); + + return "Property has been set."; + } + + return "Property is not of type Mobile."; + } + + foreach (var p in props) + { + if (p.Name.InsensitiveEquals(name)) + { + + if (!p.CanWrite) + { + return "Property is read only."; + } + + if (p.PropertyType == typeof(Mobile)) + { + p.SetValue(o, value, null); + + return "Property has been set."; + } + + return "Property is not of type Mobile."; + } + } + } + + return "Property not found."; + } + + public static string GetPropertyValue(XmlSpawner spawner, object o, string name, out Type ptype) + { + ptype = null; + if (o == null || name == null) + { + return null; + } + + var type = o.GetType(); + object po = null; + + PropertyInfo[] props; + try + { + props = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); + } + catch + { + Console.WriteLine("GetProperties error with type {0}", type); + return null; + } + + // parse the strings of the form property.attribute into two parts + // first get the property + var arglist = ParseString(name, 2, "."); + var propname = arglist[0]; + // parse up to 4 comma separated args for special keyword properties + var keywordargs = ParseString(propname, 4, ","); + + if (keywordargs[0] == "SERIAL") + { + try + { + if (o is Mobile mobile) + { + ptype = mobile.Serial.GetType(); + + return $"Serial = {mobile.Serial}"; + } + + if (o is Item item) + { + ptype = item.Serial.GetType(); + + return $"Serial = {item.Serial}"; + } + + return "Object is not item/mobile"; + } + + catch { return "Serial not found."; } + } + + if (keywordargs[0] == "TYPE") + { + ptype = typeof(Type); + + return $"Type = {o.GetType().Name}"; + + } + + // do a bit of parsing to handle array references + var arraystring = arglist[0].Split('['); + var index = -1; + if (arraystring.Length > 1) + { + // parse the property name from the indexing + propname = arraystring[0]; + + // then parse to get the index value + var arrayvalue = arraystring[1].Split(']'); + + if (arrayvalue.Length > 0) + { + if (!int.TryParse(arrayvalue[0], out index)) + { + index = -1; + } + } + } + + if (arglist.Length == 2) + { + // use the lookup table for optimization if possible + var plookup = LookupPropertyInfo(spawner, type, propname); + + if (plookup != null) + { + if (!plookup.CanRead) + { + return "Property is write only."; + } + + ptype = plookup.PropertyType; + if (ptype.IsPrimitive) + { + po = plookup.GetValue(o, null); + } + else if (ptype.GetInterface("IList") != null && index >= 0) + { + try + { + var arrayvalue = plookup.GetValue(o, null); + po = ((IList)arrayvalue)[index]; + } + catch { } + } + else + { + po = plookup.GetValue(o, null); + } + // now set the nested attribute using the new property list + return GetPropertyValue(spawner, po, arglist[1], out ptype); + } + + // is a nested property with attributes so first get the property + foreach (var p in props) + { + //if (Insensitive.Equals(p.Name, arglist[0])) + if (p.Name.InsensitiveEquals(propname)) + { + if (!p.CanRead) + { + return "Property is write only."; + } + + ptype = p.PropertyType; + if (ptype.IsPrimitive) + { + po = p.GetValue(o, null); + } + else if (ptype.GetInterface("IList") != null && index >= 0) + { + try + { + var arrayvalue = p.GetValue(o, null); + po = ((IList)arrayvalue)[index]; + } + catch { } + } + else + { + po = p.GetValue(o, null); + } + // now set the nested attribute using the new property list + return GetPropertyValue(spawner, po, arglist[1], out ptype); + } + } + } + else + { + // use the lookup table for optimization if possible + var plookup = LookupPropertyInfo(spawner, type, propname); + + if (plookup != null) + { + if (!plookup.CanRead) + { + return "Property is write only."; + } + + ptype = plookup.PropertyType; + + return InternalGetValue(o, plookup, index); + } + + // its just a simple single property + foreach (var p in props) + { + //if (Insensitive.Equals(p.Name, name)) + if (p.Name.InsensitiveEquals(propname)) + { + if (!p.CanRead) + { + return "Property is write only."; + } + + ptype = p.PropertyType; + + return InternalGetValue(o, p, index); + } + } + } + + return "Property not found."; + } + + // added in arg parsing to handle object property setting + public static bool ApplyObjectStringProperties(XmlSpawner spawner, string str, object o, Mobile trigmob, object refobject, out string status_str) + { + status_str = null; + + if (str == null || str.Length <= 0 || o == null) + { + return false; + } + + // object strings will be of the form "object/modifier" where the modifier string is of the form "propname/value/propname/value/..." + // some keywords do not have value arguments so the modifier could take the form "propname/propname/value/..." + // this is handled by parsing into both forms + + // make sure the string is properly terminated to assure proper parsing of any final keywords + var terminated = false; + str = str.Trim(); + + if (str[str.Length - 1] != '/') + { + str += "/"; + terminated = true; + } + + var arglist = ParseSlashArgs(str, 2); + + string remainder = null; + + // place the modifier section of the string in remainder + if (arglist.Length > 1) + { + remainder = arglist[1]; + } + + var no_error = true; + + // process the modifier string if there is anything + while (arglist.Length > 1) + { + // place into arglist the parsed modifier up to this point + // arglist[0] will contain the propname + // arglist[1] will contain the value + // arglist[2] will contain the reset of the modifier + arglist = ParseSlashArgs(remainder, 3); + + // singlearglist will contain the propname and the remainder + // for those keywords that do not have value args + var singlearglist = ParseSlashArgs(remainder, 2); + + if (arglist.Length > 1) + { + // handle value keywords that may take comma args + + // itemarglist[1] will contain arg2/arg3/arg4>/arg5 + // additemstr should have the full list of args /arg5 if they are there. In the case of /arg1/ADD/arg2 + // it will just have arg2 + var groupedarglist = ParseString(arglist[1], 2, "["); + string groupargstring = null; + if (groupedarglist.Length > 1) + { + // take that argument list that should like like arg2/ag3/arg4>/arg5 + // need to find the matching ">" + + var groupargs = ParseToMatchingParen(groupedarglist[1], '[', ']'); + + // and get the first part of the string without the > so itemargs[0] should be arg2/ag3/arg4 + groupargstring = groupargs[0]; + } + + // need to handle comma args that may be grouped with the () such as the (ATTACHMENT,args) arg + + //string[] value_keywordargs = ParseString(groupedarglist[0],10,","); + var value_keywordargs = groupedarglist[0].Trim().Split(','); + if (!string.IsNullOrEmpty(groupargstring)) + { + + if (value_keywordargs != null && value_keywordargs.Length > 0) + { + value_keywordargs[value_keywordargs.Length - 1] = groupargstring; + } + } + + // this quick optimization can determine whether this is a regular prop/value assignment + // since most prop modification strings will use regular propnames and not keywords, it makes sense to check for that first + if (value_keywordargs[0].Length > 0 && !char.IsUpper(value_keywordargs[0][0]) && arglist[0].Length > 0 && !char.IsUpper(arglist[0][0])) + { + // all of this code is also included in the keyword candidate tests + // this is because regular props can also be entered with uppercase so the lowercase test is not definitive + { + // check for the literal char + if (singlearglist[1] != null && singlearglist[1].Length > 0 && singlearglist[1][0] == '@') + { + //support for literal terminator + singlearglist = ParseLiteralTerminator(singlearglist[1]); + var lstr = singlearglist[0]; + if (terminated && lstr[lstr.Length - 1] == '/') + { + lstr = lstr.Remove(lstr.Length - 1, 1); + } + + var result = SetPropertyValue(spawner, o, arglist[0], lstr.Remove(0, 1)); + + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + if (singlearglist.Length > 1 && singlearglist[1] != null) + { + remainder = singlearglist[1]; + } + else + { + break; + } + } + else + { + var result = SetPropertyValue(spawner, o, arglist[0], arglist[1]); + + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + if (arglist.Length < 3) + { + break; + } + + remainder = arglist[2]; + } + } + } + else + { + if (IsValuemodKeyword(value_keywordargs[0])) + { + var kw = valuemodKeywordHash[value_keywordargs[0]]; + + if (kw == ValuemodKeyword.INC) + { + // increment the property value by the amount. Use the format propname/INC,min,max/ or propname/INC,value + if (value_keywordargs.Length > 1) + { + // get a random number + var incvalue = "0"; + if (value_keywordargs.Length > 2) + { + if (int.TryParse(value_keywordargs[1], out var min) && int.TryParse(value_keywordargs[2], out var max)) + { + incvalue = $"{Utility.RandomMinMax(min, max)}"; + } + else { status_str = $"Invalid INC args : {arglist[1]}"; no_error = false; } + } + else + { + incvalue = value_keywordargs[1]; + } + // get the current property value + var tmpvalue = GetPropertyValue(spawner, o, arglist[0], out var ptype); + + // see if it was successful + if (ptype == null) + { + status_str = $"Cant find {arglist[0]}"; + no_error = false; + } + else + { + var currentvalue = "0"; + try + { + var arglist2 = ParseString(tmpvalue, 2, "="); + var arglist3 = ParseString(arglist2[1], 2, " "); + currentvalue = arglist3[0].Trim(); + } + catch { } + var tmpstr = currentvalue; + + // should use the actual ptype info to do the addition. Maybe later. + if (double.TryParse(currentvalue, NumberStyles.Any, CultureInfo.InvariantCulture, out var d0) && double.TryParse(incvalue, NumberStyles.Any, CultureInfo.InvariantCulture, out var d1)) + { + tmpstr = ((int)(d0 + d1)).ToString(); + } + else + { status_str = $"Invalid INC args : {arglist[1]}"; no_error = false; } + + // set the property value using the incremented value + var result = SetPropertyValue(spawner, o, arglist[0], tmpstr); + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + } + } + else + { + status_str = $"Invalid INC args : {arglist[1]}"; + no_error = false; + } + if (arglist.Length < 3) + { + break; + } + + remainder = arglist[2]; + } + else if (kw == ValuemodKeyword.MOB) + { + // lookup the mob id based on the name. format is /MOB,name[,type]/ + if (value_keywordargs.Length > 1) + { + string typestr = null; + if (value_keywordargs.Length > 2) + { + typestr = value_keywordargs[2]; + } + // lookup the name + Mobile mob_id = null; + try + { + mob_id = FindMobileByName(spawner, value_keywordargs[1], typestr); // the format of this will be 0xvalue "name" + } + catch { status_str = $"Invalid MOB args : {arglist[1]}"; no_error = false; } + // set the property value using this format (M) id name + + var result = SetPropertyObject(spawner, o, arglist[0], mob_id); + + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + } + else + { + no_error = false; + } + + if (arglist.Length < 3) + { + break; + } + + remainder = arglist[2]; + } + else if (kw == ValuemodKeyword.TRIGMOB) + { + var result = SetPropertyObject(spawner, o, arglist[0], trigmob); + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + if (arglist.Length < 3) + { + break; + } + + remainder = arglist[2]; + } + else if (kw == ValuemodKeyword.PLAYERSINRANGE) + { + // syntax is PLAYERSINRANGE,range + var nplayers = 0; + var range = 0; + // get the number of players in range + if (value_keywordargs.Length > 1) + { + _ = int.TryParse(value_keywordargs[1], out range); + } + + // count nearby players + if (refobject is Item item) + { + foreach (var p in item.GetMobilesInRange(range)) + { + if (p.Player && p.AccessLevel == AccessLevel.Player) + { + nplayers++; + } + } + } + else if (refobject is Mobile mobile) + { + foreach (var p in mobile.GetMobilesInRange(range)) + { + if (p.Player && p.AccessLevel == AccessLevel.Player) + { + nplayers++; + } + } + } + + var result = SetPropertyValue(spawner, o, arglist[0], nplayers.ToString()); + + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + if (arglist.Length < 3) + { + break; + } + + remainder = arglist[2]; + } + } + else + { + // check for the literal char + if (singlearglist[1] != null && singlearglist[1].Length > 0 && singlearglist[1][0] == '@') + { + //support for literal terminator + singlearglist = ParseLiteralTerminator(singlearglist[1]); + var lstr = singlearglist[0]; + if (terminated && lstr[lstr.Length - 1] == '/') + { + lstr = lstr.Remove(lstr.Length - 1, 1); + } + + var result = SetPropertyValue(spawner, o, arglist[0], lstr.Remove(0, 1)); + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + if (singlearglist.Length > 1 && singlearglist[1] != null) + { + remainder = singlearglist[1]; + } + else + { + break; + } + } + else + { + var result = SetPropertyValue(spawner, o, arglist[0], arglist[1]); + // see if it was successful + if (result != "Property has been set.") + { + status_str = $"{arglist[0]} : {result}"; + no_error = false; + } + if (arglist.Length < 3) + { + break; + } + + remainder = arglist[2]; + } + } + } + } + } + return no_error; + } + + public static bool TestMobProperty(XmlSpawner spawner, Mobile mobile, string testString, out string status_str) + { + status_str = null; + // now make sure the mobile itself is there + if (mobile == null || mobile.Deleted) + { + return false; + } + + var testreturn = CheckPropertyString(spawner, mobile, testString, out status_str); + + return testreturn; + } + + public static bool TestItemProperty(XmlSpawner spawner, Item ObjectPropertyItem, string testString, out string status_str) + { + // now make sure the item itself is there + if (ObjectPropertyItem == null || ObjectPropertyItem.Deleted) + { + status_str = "Trigger Object not found"; + return false; + } + + var testreturn = CheckPropertyString(spawner, ObjectPropertyItem, testString, out status_str); + + return testreturn; + } + + public static PropertyInfo LookupPropertyInfo(XmlSpawner spawner, Type type, string propname) + { + if (spawner == null || type == null || propname == null) + { + return null; + } + + // look up the info in the current list + + spawner.PropertyInfoList ??= new List(); + + PropertyInfo pinfo = null; + TypeInfo tinfo = null; + + foreach (var to in spawner.PropertyInfoList) + { + // check the type + if (to.t == type) + { + // found it + tinfo = to; + + // now search the property list + foreach (var p in to.plist) + { + if (p.Name.InsensitiveEquals(propname)) + { + pinfo = p; + } + } + } + } + + // did we find the property? + if (pinfo != null) + { + return pinfo; + } + // if it cant be found, then do the full search and add it to the list + + var props = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); + + foreach (var p in props) + { + if (p.Name.InsensitiveEquals(propname)) + { + // did we find the type at least? + if (tinfo == null) + { + // if not then add the type to the list + tinfo = new TypeInfo + { + t = type + }; + + spawner.PropertyInfoList.Add(tinfo); + } + + // and add the property to the tinfo property list + tinfo.plist.Add(p); + return p; + } + } + + return null; + } + + public static string ParseForKeywords(XmlSpawner spawner, object o, string valstr, bool literal, out Type ptype) + { + ptype = null; + + if (valstr == null || valstr.Length <= 0) + { + return null; + } + + var str = valstr.Trim(); + + // look for keywords + // need to handle the case of nested arglists like arg,arg, + // handle value keywords that may take comma args + + // itemarglist[1] will contain arg2/arg3/arg4>/arg5 + // additemstr should have the full list of args /arg5 if they are there. In the case of /arg1/ADD/arg2 + // it will just have arg2 + var groupedarglist = ParseString(str, 2, "["); + string groupargstring = null; + if (groupedarglist.Length > 1) + { + // take that argument list that should like like arg2/ag3/arg4>/arg5 + // need to find the matching ">" + + var groupargs = ParseToMatchingParen(groupedarglist[1], '[', ']'); + + // and get the first part of the string without the > so itemargs[0] should be arg2/ag3/arg4 + groupargstring = groupargs[0]; + } + + // need to handle comma args that may be grouped with the () such as the (ATTACHMENT,args) arg + var arglist = groupedarglist[0].Trim().Split(','); + + if (!string.IsNullOrEmpty(groupargstring) && arglist.Length > 0) + { + arglist[arglist.Length - 1] = groupargstring; + } + + var pname = arglist[0].Trim(); + var startc = str[0]; + + // first see whether it is a standard numeric value + if (startc is '.' or '-' or '+' or >= '0' and <= '9') + { + // determine the type + ptype = str.Contains('.') ? typeof(double) : typeof(int); + + return str; + } + + if (startc is '"' or '(') + { + ptype = typeof(string); + return str; + } + + if (startc == '#') + { + ptype = typeof(string); + return str.Substring(1); + } + // or a bool + + if (str.ToLower() is "true" or "false") + { + ptype = typeof(bool); + return str; + } + // then look for a keyword + + if (IsValueKeyword(pname)) + { + var kw = valueKeywordHash[pname]; + + if (kw == ValueKeyword.PLAYERSINRANGE && arglist.Length > 1) + { + // syntax is PLAYERSINRANGE,range + + ptype = typeof(int); + + var nplayers = 0; + // get the number of players in range + _ = int.TryParse(arglist[1], out var range); + + // count nearby players + if (spawner?.SpawnRegion != null && range < 0) + { + foreach (var p in spawner.SpawnRegion.GetPlayers()) + { + if (p.AccessLevel <= spawner.TriggerAccessLevel) + { + nplayers++; + } + } + } + else if (o is Item item) + { + foreach (var p in item.GetMobilesInRange(range)) + { + if (p.Player && p.AccessLevel == AccessLevel.Player) + { + nplayers++; + } + } + } + else if (o is Mobile mobile) + { + foreach (var p in mobile.GetMobilesInRange(range)) + { + if (p.Player && p.AccessLevel == AccessLevel.Player) + { + nplayers++; + } + } + } + + return nplayers.ToString(); + } + if (kw == ValueKeyword.RANDNAME && arglist.Length > 1) + { + // syntax is RANDNAME,nametype + return NameList.RandomName(arglist[1]); + } + + // an invalid keyword format will be passed as literal + return str; + } + + if (literal) + { + ptype = typeof(string); + return str; + } + + // otherwise treat it as a property name + var result = GetPropertyValue(spawner, o, pname, out ptype); + + return ParseGetValue(result, ptype); + } + + public static string ParseGetValue(string str, Type ptype) + { + // the results of getPropertyValue takes the form + // propname = value + // or + // propname = value (hexvalue) + + if (str == null) + { + return null; + } + + // find the separator + var arglist = str.Split("=".ToCharArray(), 2); + + if (arglist.Length > 1) + { + if (IsNumeric(ptype)) + { + // parse the value portion and get rid of the possible (hexvalue) portion of the string + var arglist2 = arglist[1].Trim().Split(" ".ToCharArray(), 2); + + return arglist2[0]; + } + + // for everything else + // pass on as is + return arglist[1].Trim(); + } + + return null; + } + + public static bool CheckPropertyString(XmlSpawner spawner, object o, string testString, out string status_str) + { + status_str = null; + + if (o == null) + { + return false; + } + + if (string.IsNullOrEmpty(testString)) + { + status_str = "Null property test string"; + return false; + } + // parse the property test string for and(&)/or(|) operators + var arglist = ParseString(testString, 2, "&|"); + if (arglist.Length < 2) + { + var returnval = CheckSingleProperty(spawner, o, testString, out status_str); + + // simple conditional test with no and/or operators + return returnval; + } + + // test each half independently and combine the results + var first = CheckSingleProperty(spawner, o, arglist[0], out _); + + // this will recursively parse the property test string with implicit nesting for multiple logical tests of the + // form A * B * C * D being grouped as A * (B * (C * D)) + var second = CheckPropertyString(spawner, o, arglist[1], out status_str); + + var andposition = testString.IndexOf("&"); + var orposition = testString.IndexOf("|"); + + // combine them based upon the operator + if (andposition > 0 && orposition <= 0 || andposition > 0 && andposition < orposition) + { + // and operator + return first && second; + } + + if (orposition > 0 && andposition <= 0 || orposition > 0 && orposition < andposition) + { + // or operator + return first || second; + } + + // should never get here + return false; + } + + public static bool CheckSingleProperty(XmlSpawner spawner, object o, string testString, out string status_str) + { + status_str = null; + + if (o == null || testString == null || testString.Length == 0) + { + return false; + } + + //get the prop name and test value + // format will be prop=prop, or prop>prop, prop 0 && testString[0] == '~') + { + invertreturn = true; + testString = testString.Substring(1, testString.Length - 1); + } + + var arglist = ParseString(testString, 2, "=> 0) + { + hasequal = true; + } + else + if (testString.IndexOf("!") > 0) + { + hasnotequals = true; + } + else + if (testString.IndexOf(">") > 0) + { + hasgreaterthan = true; + } + else + if (testString.IndexOf("<") > 0) + { + haslessthan = true; + } + + // does it have a valid operator? + if (!hasequal && !hasgreaterthan && !haslessthan && !hasnotequals) + { + return false; + } + + var value1 = ParseForKeywords(spawner, o, arglist[0].Trim(), false, out var ptype1); + + // see if it was successful + if (ptype1 == null) + { + status_str = $"{arglist[0]} : {value1}"; + + return invertreturn; + //return false; + } + + var value2 = ParseForKeywords(spawner, o, arglist[1].Trim(), false, out var ptype2); + + // see if it was successful + if (ptype2 == null) + { + status_str = $"{arglist[1]} : {value2}"; + + return invertreturn; + //return false; + } + + // look for hex numeric specifications + var base1 = 10; + var base2 = 10; + if (IsNumeric(ptype1) && !string.IsNullOrEmpty(value1) && value1.StartsWith("0x")) + { + base1 = 16; + } + + if (IsNumeric(ptype2) && !string.IsNullOrEmpty(value2) && value2.StartsWith("0x")) + { + base2 = 16; + } + + // and do the type dependent comparisons + if (ptype2 == typeof(TimeSpan) || ptype1 == typeof(TimeSpan)) + { + if (hasequal) + { + if (TimeSpan.TryParse(value1, out var ts1) && TimeSpan.TryParse(value2, out var ts2)) + { + if (ts1 == ts2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid timespan comparison : {{0}}{testString}"; + } + } + else if (hasnotequals) + { + if (TimeSpan.TryParse(value1, out var ts1) && TimeSpan.TryParse(value2, out var ts2)) + { + if (ts1 != ts2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid timespan comparison : {{0}}{testString}"; + } + } + else if (hasgreaterthan) + { + if (TimeSpan.TryParse(value1, out var ts1) && TimeSpan.TryParse(value2, out var ts2)) + { + if (ts1 > ts2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid timespan comparison : {{0}}{testString}"; + } + } + else + { + if (TimeSpan.TryParse(value1, out var ts1) && TimeSpan.TryParse(value2, out var ts2)) + { + if (ts1 < ts2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid timespan comparison : {{0}}{testString}"; + } + } + } + else + // and do the type dependent comparisons + if (ptype2 == typeof(DateTime) || ptype1 == typeof(DateTime)) + { + if (hasequal) + { + if (DateTime.TryParse(value1, out var dt1) && DateTime.TryParse(value2, out var dt2)) + { + if (dt1 == dt2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid DateTime comparison : {{0}}{testString}"; + } + } + else if (hasnotequals) + { + if (DateTime.TryParse(value1, out var dt1) && DateTime.TryParse(value2, out var dt2)) + { + if (dt1 != dt2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid DateTime comparison : {{0}}{testString}"; + } + } + else if (hasgreaterthan) + { + if (DateTime.TryParse(value1, out var dt1) && DateTime.TryParse(value2, out var dt2)) + { + if (dt1 > dt2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid DateTime comparison : {{0}}{testString}"; + } + } + else + { + if (DateTime.TryParse(value1, out var dt1) && DateTime.TryParse(value2, out var dt2)) + { + if (dt1 < dt2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid DateTime comparison : {{0}}{testString}"; + } + } + } + else if (IsNumeric(ptype2) && IsNumeric(ptype1)) + { + if (hasequal) + { + try + { + if (Convert.ToInt64(value1, base1) == Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasnotequals) + { + try + { + if (Convert.ToInt64(value1, base1) != Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasgreaterthan) + { + try + { + if (Convert.ToInt64(value1, base1) > Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + else + { + try + { + if (Convert.ToInt64(value1, base1) < Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + } + else if (ptype2 == typeof(double) && IsNumeric(ptype1)) + { + if (hasequal) + { + try + { + if (Convert.ToInt64(value1, base1) == double.Parse(value2)) + { + return !invertreturn; + } + } + catch + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasnotequals) + { + try + { + if (Convert.ToInt64(value1, base1) != double.Parse(value2)) + { + return !invertreturn; + } + } + catch + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasgreaterthan) + { + try + { + if (Convert.ToInt64(value1, base1) > double.Parse(value2)) + { + return !invertreturn; + } + } + catch { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + else + { + try + { + if (Convert.ToInt64(value1, base1) < double.Parse(value2)) + { + return !invertreturn; + } + } + catch { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + } + else if (ptype1 == typeof(double) && IsNumeric(ptype2)) + { + if (hasequal) + { + try + { + if (double.Parse(value1) == Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasnotequals) + { + try + { + if (double.Parse(value1) != Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasgreaterthan) + { + try + { + if (double.Parse(value1) > Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + else + { + try + { + if (double.Parse(value1) < Convert.ToInt64(value2, base2)) + { + return !invertreturn; + } + } + catch { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + } + else if (ptype1 == typeof(double) && ptype2 == typeof(double)) + { + double val1; + double val2; + if (hasequal) + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 == val2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasnotequals) + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 != val2) + { + return !invertreturn; + } + } + else + { + status_str = $"invalid int comparison : {{0}}{testString}"; + } + } + else if (hasgreaterthan) + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 > val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + else + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 < val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid int comparison : {{0}}{testString}"; } + } + } + else if (ptype2 == typeof(bool) && ptype1 == typeof(bool)) + { + bool val1, val2; + if (hasequal) + { + if (bool.TryParse(value1, out val1) && bool.TryParse(value2, out val2)) + { + if (val1 == val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid bool comparison : {{0}}{testString}"; } + } + else if (hasnotequals) + { + if (bool.TryParse(value1, out val1) && bool.TryParse(value2, out val2)) + { + if (val1 != val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid bool comparison : {{0}}{testString}"; } + } + } + else if (ptype2 == typeof(double) || ptype2 == typeof(double)) + { + double val1; + double val2; + if (hasequal) + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 == val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid double comparison : {{0}}{testString}"; } + } + else if (hasnotequals) + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 != val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid double comparison : {{0}}{testString}"; } + } + else if (hasgreaterthan) + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 > val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid double comparison : {{0}}{testString}"; } + } + else + { + if (double.TryParse(value1, NumberStyles.Any, CultureInfo.InvariantCulture, out val1) && double.TryParse(value2, NumberStyles.Any, CultureInfo.InvariantCulture, out val2)) + { + if (val1 < val2) + { + return !invertreturn; + } + } + else { status_str = $"invalid double comparison : {{0}}{testString}"; } + } + } + else + { + // by default just do a string comparison + if (hasequal) + { + if (value1 == value2) + { + return !invertreturn; + } + } + else + if (hasnotequals) + { + if (value1 != value2) + { + return !invertreturn; + } + } + } + return invertreturn; + } + +#if XML_QUESTS + + public static Item SearchMobileForItem(Mobile m, string targetName, string typeStr, bool searchbank) + { + return SearchMobileForItem(m, targetName, typeStr, searchbank, false); + } + + public static Item SearchMobileForItem(Mobile m, string targetName, string typeStr, bool searchbank, bool equippedonly) + { + + if (m != null && !m.Deleted) + { + // go through all of the items in the pack + var packlist = m.Items; + + for (var i = 0; i < packlist.Count; ++i) + { + var item = packlist[i]; + + // dont search bank boxes + if (item is BankBox && !searchbank && !equippedonly) + { + continue; + } + + // recursively search containers + if (item != null && !item.Deleted) + { + if (item is Container container && !equippedonly) + { + var itemTarget = SearchPackForItem(container, targetName, typeStr); + + if (itemTarget != null) + { + return itemTarget; + } + } + // test the item name against the trigger string + // if a typestring has been specified then check against that as well + if (CheckNameMatch(targetName, item.Name)) + { + + if (typeStr == null || CheckType(item, typeStr)) + { + //found it + return item; + } + } + } + } + // now check any item that might be held + var held = m.Holding; + + if (held != null && !held.Deleted && !equippedonly) + { + if (held is Container container) + { + var itemTarget = SearchPackForItem(container, targetName, typeStr); + + if (itemTarget != null) + { + return itemTarget; + } + } + // test the item name against the trigger string + if (CheckNameMatch(targetName, held.Name)) + { + if (typeStr == null || CheckType(held, typeStr)) + { + //found it + return held; + } + } + } + } + return null; + } + public static Item SearchPackForItem(Container pack, string targetName, string typestr) + { + if (pack != null && !pack.Deleted) + { + Type targettype = null; + + if (typestr != null) + { + targettype = AssemblyHandler.FindTypeByName(typestr); + } + + // go through all of the items in the pack + var packlist = pack.Items; + + for (var i = 0; i < packlist.Count; ++i) + { + var item = packlist[i]; + + if (item != null && !item.Deleted) + { + + if (item is Container container) + { + var itemTarget = SearchPackForItem(container, targetName, typestr); + + if (itemTarget != null) + { + return itemTarget; + } + } + // test the item name against the trigger string + if (CheckNameMatch(targetName, item.Name)) + { + if (targettype == null || item.GetType().Equals(targettype) || item.GetType().IsSubclassOf(targettype)) + { + //found it + return item; + } + } + } + } + } + return null; + } + private static bool CheckNameMatch(string targetname, string name) + { + // a "*" targetname will match anything + // a null or empty targetname will match a null name + // otherwise the strings must match + return targetname == "*" || name == targetname || targetname != null && targetname.Length == 0 && name == null; + } + + public static bool CheckType(object o, string typename) + { + if (typename == null || o == null) + { + return false; + } + + // test the type + var objecttype = o.GetType(); + + Type targettype = null; + + try + { + targettype = AssemblyHandler.FindTypeByName(typename); + } + catch { } + + if (objecttype != null && targettype != null && (objecttype.Equals(targettype) || objecttype.IsSubclassOf(targettype))) + { + return true; + + } + + return false; + + } + public static bool CheckForCarried(Mobile m, string objectivestr) + { + if (m == null || objectivestr == null) + { + return true; + } + + // parse the objective string that might be of the form 'obj &| obj &| obj ...' + var arglist = ParseString(objectivestr, 2, "&|"); + if (arglist.Length < 2) + { + // simple test with no and/or operators + return SingleCheckForCarried(m, objectivestr); + } + + // test each half independently and combine the results + var first = SingleCheckForCarried(m, arglist[0]); + + // this will recursively parse the property test string with implicit nesting for multiple logical tests of the + // form A * B * C * D being grouped as A * (B * (C * D)) + var second = CheckForCarried(m, arglist[1]); + + var andposition = objectivestr.IndexOf("&"); + var orposition = objectivestr.IndexOf("|"); + + // combine them based upon the operator + if (andposition > 0 && orposition <= 0 || andposition > 0 && andposition < orposition) + { + // and operator + return first && second; + } + + if (orposition > 0 && andposition <= 0 || orposition > 0 && orposition < andposition) + { + // or operator + return first || second; + } + // should never get here + return false; + } + public static bool SingleCheckForCarried(Mobile m, string objectivestr) + { + + if (m == null || objectivestr == null) + { + return false; + } + + var has_valid_item = false; + + // check to see whether there is an objective specification as well. The format is name[,type][,EQUIPPED][,objective,objective,...] + var objstr = ParseString(objectivestr, 8, ","); + + var itemname = objstr[0]; + + // check for attachment keyword + if (itemname == "ATTACHMENT") + { +#if XML_ATTACH + // syntax is ATTACHMENT,name,type + if (objstr.Length > 1) + { + var aname = objstr[1]; + Type atype = null; + if (objstr.Length > 2) + { + try + { + atype = AssemblyHandler.FindTypeByName(objstr[2]); + } + catch { } + } + + // try to find the attachment on the mob + if (XmlAttach.FindAttachmentOnMobile(m, atype, aname) != null) + { + return true; + } + + return false; + } +#endif + + return false; + } + + var equippedonly = false; + string typestr = null; + var objoffset = 1; + // is there a type specification? + + while (objoffset < objstr.Length) + { + if (objstr[objoffset] != null && objstr[objoffset].Length > 0) + { + + var startc = objstr[objoffset][0]; + + if (startc is >= '0' and <= '9') + { + // this is the start of the numeric objective specifications + break; + } + + if (objstr[objoffset] == "EQUIPPED") + { + equippedonly = true; + } + else + { + // treat as a type specification if it does not begin with a numeric char + // and is not the EQUIPPED keyword + typestr = objstr[objoffset]; + } + } + objoffset++; + } + + var testitem = SearchMobileForItem(m, itemname, typestr, false, equippedonly); + + // found the item + if (testitem != null) + { + // check to see if it is a quest token item. If so, then check validity, otherwise just finding it is enough + if (testitem is IXmlQuest token) + { + if (token.IsValid) + { + if (objstr.Length > objoffset) + { + has_valid_item = true; + // get any objectives and test for them. If any of the required conditions are false, then dont trigger + for (var n = objoffset; n < objstr.Length; n++) + { + try + { + switch (int.Parse(objstr[n]) - objoffset + 1) + { + case 1: + { + if (!token.Completed1) + { + has_valid_item = false; + } + + break; + } + case 2: + { + if (!token.Completed2) + { + has_valid_item = false; + } + + break; + } + case 3: + { + if (!token.Completed3) + { + has_valid_item = false; + } + + break; + } + case 4: + { + if (!token.Completed4) + { + has_valid_item = false; + } + + break; + } + case 5: + { + if (!token.Completed5) + { + has_valid_item = false; + } + + break; + } + } + } + catch { } + } + } + else + // if an objective list has not been specified then just a valid item is enough + { + has_valid_item = true; + } + } + } + else + { + // is the equippedonly flag set? If so then see if the item is equipped + if (equippedonly && testitem.Parent == m || !equippedonly) + { + has_valid_item = true; + } + } + } + return has_valid_item; + } + public static bool CheckForNotCarried(Mobile m, string objectivestr) + { + if (m == null || objectivestr == null) + { + return true; + } + + // parse the objective string that might be of the form 'obj &| obj &| obj ...' + var arglist = ParseString(objectivestr, 2, "&|"); + if (arglist.Length < 2) + { + // simple test with no and/or operators + return SingleCheckForNotCarried(m, objectivestr); + } + + // test each half independently and combine the results + var first = SingleCheckForNotCarried(m, arglist[0]); + + // this will recursively parse the property test string with implicit nesting for multiple logical tests of the + // form A * B * C * D being grouped as A * (B * (C * D)) + var second = CheckForNotCarried(m, arglist[1]); + + var andposition = objectivestr.IndexOf("&"); + var orposition = objectivestr.IndexOf("|"); + + // for the & operator + // notrigger if + // notcarrying A | notcarrying B + // people will actually think of it as not(carrying A | carrying B) + // which is + // notrigger if + // notcarrying A && notcarrying B + // similarly for the & operator + + // combine them based upon the operator + if (andposition > 0 && orposition <= 0 || andposition > 0 && andposition < orposition) + { + // and operator (see explanation above) + return first || second; + } + + if (orposition > 0 && andposition <= 0 || orposition > 0 && orposition < andposition) + { + // or operator (see explanation above) + return first && second; + } + // should never get here + return false; + } + + public static bool SingleCheckForNotCarried(Mobile m, string objectivestr) + { + + if (m == null || objectivestr == null) + { + return true; + } + + var has_no_such_item = true; + + // check to see whether there is an objective specification as well. The format is name[,type][,EQUIPPED][,objective,objective,...] + var objstr = ParseString(objectivestr, 8, ","); + var itemname = objstr[0]; + + // check for attachment keyword + if (itemname == "ATTACHMENT") + { +#if XML_ATTACH + // syntax is ATTACHMENT,name,type + if (objstr.Length > 1) + { + var aname = objstr[1]; + Type atype = null; + if (objstr.Length > 2) + { + try + { + atype = AssemblyHandler.FindTypeByName(objstr[2]); + } + catch { } + } + + // try to find the attachment on the mob + if (XmlAttach.FindAttachmentOnMobile(m, atype, aname) != null) + { + return false; + } + + return true; + } +#endif + + return true; + } + + var equippedonly = false; + string typestr = null; + var objoffset = 1; + // is there a type specification? + + while (objoffset < objstr.Length) + { + if (objstr[objoffset] != null && objstr[objoffset].Length > 0) + { + + var startc = objstr[objoffset][0]; + + if (startc is >= '0' and <= '9') + { + // this is the start of the numeric objective specifications + break; + } + + if (objstr[objoffset] == "EQUIPPED") + { + equippedonly = true; + } + else + { + // treat as a type specification if it does not begin with a numeric char + // and is not the EQUIPPED keyword + typestr = objstr[objoffset]; + } + } + objoffset++; + } + + // look for the item + var testitem = SearchMobileForItem(m, itemname, typestr, false, equippedonly); + + // found the item + if (testitem != null) + { + // check to see if it is a quest token item. If so, then check validity, otherwise just finding it is enough + if (testitem is IXmlQuest token && token.IsValid) + { + if (objstr.Length > objoffset) + { + has_no_such_item = true; + // get any objectives and test for them. If any of the required conditions are true, then block trigger + for (var n = objoffset; n < objstr.Length; n++) + { + try + { + switch (int.Parse(objstr[n]) - objoffset + 1) + { + case 1: + { + if (token.Completed1) + { + has_no_such_item = false; + } + + break; + } + case 2: + { + if (token.Completed2) + { + has_no_such_item = false; + } + + break; + } + case 3: + { + if (token.Completed3) + { + has_no_such_item = false; + } + + break; + } + case 4: + { + if (token.Completed4) + { + has_no_such_item = false; + } + + break; + } + case 5: + { + if (token.Completed5) + { + has_no_such_item = false; + } + + break; + } + } + } + catch { } + } + } + else + { + has_no_such_item = false; + } + } + else + { + // is the equippedonly flag set? If so then see if the item is equipped + if (equippedonly && testitem.Parent == m || !equippedonly) + { + has_no_such_item = false; + } + } + } + return has_no_such_item; + } +#endif + + public static Item FindItemByName(XmlSpawner fromspawner, string name, string typestr) + { + if (name == null) + { + return null; + } + + var count = 0; + + var founditem = FindInRecentItemSearchList(fromspawner, name, typestr); + + if (founditem != null) + { + return founditem; + } + + Type targettype = null; + if (typestr != null) + { + targettype = AssemblyHandler.FindTypeByName(typestr); + } + + // search through all items in the world and find the first one with a matching name + foreach (var item in World.Items.Values) + { + var itemtype = item.GetType(); + + if (!item.Deleted && (name.Length == 0 || string.Compare(item.Name, name, true) == 0)) + { + + if (typestr == null || + targettype != null && (itemtype.Equals(targettype) || itemtype.IsSubclassOf(targettype))) + { + founditem = item; + count++; + // added the break in to return the first match instead of forcing uniqueness (overrides the count test) + break; + } + } + } + + if (count == 1) // if a unique item is found then success + { + // add this to the recent search list + AddToRecentItemSearchList(fromspawner, founditem); + + return founditem; + } + + return null; + } + + public static Mobile FindMobileByName(XmlSpawner fromspawner, string name, string typestr) + { + if (name == null) + { + return null; + } + + var count = 0; + + var foundmobile = FindInRecentMobileSearchList(fromspawner, name, typestr); + + if (foundmobile != null) + { + return foundmobile; + } + + Type targettype = null; + if (typestr != null) + { + targettype = AssemblyHandler.FindTypeByName(typestr); + } + + // search through all mobiles in the world and find one with a matching name + foreach (var mobile in World.Mobiles.Values) + { + var mobtype = mobile.GetType(); + if (!mobile.Deleted && (name.Length == 0 || string.Compare(mobile.Name, name, true) == 0) && (typestr == null || + targettype != null && (mobtype.Equals(targettype) || mobtype.IsSubclassOf(targettype)))) + { + foundmobile = mobile; + count++; + // added the break in to return the first match instead of forcing uniqueness (overrides the count test) + break; + } + } + + // if a unique item is found then success + if (count == 1) + { + // add this to the recent search list + AddToRecentMobileSearchList(fromspawner, foundmobile); + + return foundmobile; + } + + return null; + } + + public static XmlSpawner FindSpawnerByName(XmlSpawner fromspawner, string name) + { + if (name == null) + { + return null; + } + + if (name.StartsWith("0x")) + { + uint serial; + try + { + serial = Convert.ToUInt32(name, 16); + return World.FindEntity((Serial)serial); + } + catch { } + } + + // do a quick search through the recent search list to see if it is there + var foundspawner = FindInRecentSpawnerSearchList(fromspawner, name); + + if (foundspawner != null) + { + return foundspawner; + } + + var count = 0; + + // search through all xmlspawners in the world and find one with a matching name + foreach (var item in World.Items.Values) + { + if (item is XmlSpawner spawner) + { + if (!spawner.Deleted && string.Compare(spawner.Name, name, true) == 0) + { + foundspawner = spawner; + + count++; + // added the break in to return the first match instead of forcing uniqueness (overrides the count test) + break; + } + } + } + + // if a unique item is found then success + if (count == 1) + { + // add this to the recent search list + AddToRecentSpawnerSearchList(fromspawner, foundspawner); + + return foundspawner; + } + + return null; + } + + public static void AddToRecentSpawnerSearchList(XmlSpawner spawner, XmlSpawner target) + { + if (spawner == null || target == null) + { + return; + } + + spawner.RecentSpawnerSearchList ??= new List(); + spawner.RecentSpawnerSearchList.Add(target); + + // check the length and truncate if it gets too long + if (spawner.RecentSpawnerSearchList.Count > 100) + { + spawner.RecentSpawnerSearchList.RemoveAt(0); + } + } + + public static XmlSpawner FindInRecentSpawnerSearchList(XmlSpawner spawner, string name) + { + if (spawner == null || name == null || spawner.RecentSpawnerSearchList == null) + { + return null; + } + + List deletelist = null; + XmlSpawner foundspawner = null; + + foreach (var s in spawner.RecentSpawnerSearchList) + { + if (s.Deleted) + { + // clean it up + deletelist ??= new List(); + + deletelist.Add(s); + } + else + if (string.Compare(s.Name, name, true) == 0) + { + foundspawner = s; + break; + } + } + + if (deletelist != null) + { + foreach (var i in deletelist) + { + _ = spawner.RecentSpawnerSearchList.Remove(i); + } + } + + return foundspawner; + } + + public static void AddToRecentItemSearchList(XmlSpawner spawner, Item target) + { + if (spawner == null || target == null) + { + return; + } + + spawner.RecentItemSearchList ??= new List(); + + spawner.RecentItemSearchList.Add(target); + + // check the length and truncate if it gets too long + if (spawner.RecentItemSearchList.Count > 100) + { + spawner.RecentItemSearchList.RemoveAt(0); + } + } + + public static Item FindInRecentItemSearchList(XmlSpawner spawner, string name, string typestr) + { + if (spawner == null || name == null || spawner.RecentItemSearchList == null) + { + return null; + } + + List deletelist = null; + Item founditem = null; + + Type targettype = null; + if (typestr != null) + { + targettype = AssemblyHandler.FindTypeByName(typestr); + } + + foreach (var item in spawner.RecentItemSearchList) + { + if (item.Deleted) + { + // clean it up + deletelist ??= new List(); + + deletelist.Add(item); + } + else + if (name.Length == 0 || string.Compare(item.Name, name, true) == 0) + { + if (typestr == null || + targettype != null && (item.GetType().Equals(targettype) || item.GetType().IsSubclassOf(targettype))) + { + founditem = item; + break; + } + } + } + + if (deletelist != null) + { + foreach (var i in deletelist) + { + _ = spawner.RecentItemSearchList.Remove(i); + } + } + + return founditem; + } + + public static void AddToRecentMobileSearchList(XmlSpawner spawner, Mobile target) + { + if (spawner == null || target == null) + { + return; + } + + spawner.RecentMobileSearchList ??= new List(); + + spawner.RecentMobileSearchList.Add(target); + + // check the length and truncate if it gets too long + if (spawner.RecentMobileSearchList.Count > 100) + { + spawner.RecentMobileSearchList.RemoveAt(0); + } + } + + public static Mobile FindInRecentMobileSearchList(XmlSpawner spawner, string name, string typestr) + { + if (spawner == null || name == null || spawner.RecentMobileSearchList == null) + { + return null; + } + + List deletelist = null; + Mobile foundmobile = null; + + Type targettype = null; + if (typestr != null) + { + targettype = AssemblyHandler.FindTypeByName(typestr); + } + + foreach (var m in spawner.RecentMobileSearchList) + { + if (m.Deleted) + { + // clean it up + deletelist ??= new List(); + + deletelist.Add(m); + } + else + if (name.Length == 0 || string.Compare(m.Name, name, true) == 0) + { + + if (typestr == null || + targettype != null && (m.GetType().Equals(targettype) || m.GetType().IsSubclassOf(targettype))) + { + foundmobile = m; + break; + } + } + } + + if (deletelist != null) + { + foreach (var i in deletelist) + { + _ = spawner.RecentMobileSearchList.Remove(i); + } + } + + return foundmobile; + } + + public static string ApplySubstitution(XmlSpawner spawner, object o, string typeName) + { + var sb = new System.Text.StringBuilder(); + + // go through the string looking for instances of {keyword} + var remaining = typeName; + + while (!string.IsNullOrEmpty(remaining)) + { + + var startindex = remaining.IndexOf('{'); + + if (startindex == -1 || startindex + 1 >= remaining.Length) + { + // if there are no more delimiters then append the remainder and finish + _ = sb.Append(remaining); + break; + } + + // might be a substitution, check for keywords + var endindex = remaining.Substring(startindex + 1).IndexOf("}"); + + // if the ending delimiter cannot be found then just append and finish + if (endindex == -1) + { + _ = sb.Append(remaining); + break; + } + + // get the string up to the delimiter + var firstpart = remaining.Substring(0, startindex); + _ = sb.Append(firstpart); + + var keypart = remaining.Substring(startindex + 1, endindex); + + // try to evaluate and then substitute the arg + + var value = ParseForKeywords(spawner, o, keypart.Trim(), true, out _); + + // trim off the " from strings + if (value != null) + { + value = value.Trim('"'); + } + + // replace the parsed value for the keyword + _ = sb.Append(value); + + // continue processing the rest of the string + if (endindex + startindex + 2 >= remaining.Length) + { + break; + } + + remaining = remaining.Substring(endindex + startindex + 2, remaining.Length - endindex - startindex - 2); + } + return sb.ToString(); + } + + public static string ParseObjectType(string str) + { + var arglist = ParseSlashArgs(str, 2); + if (arglist != null && arglist.Length > 0) + { + // parse out any arguments of the form typename,arg,arg,.. + var typeargs = ParseCommaArgs(arglist[0], 2); + if (typeargs.Length > 1) + { + return typeargs[0]; + } + return arglist[0]; + } + + return null; + } + + public static string[] ParseObjectArgs(string str) + { + var arglist = ParseSlashArgs(str, 2); + if (arglist.Length > 0) + { + var itemtypestring = arglist[0]; + // parse out any arguments of the form typename,arg,arg,.. + // find the first arg if it is there + string[] typeargs = null; + var argstart = 0; + if (!string.IsNullOrEmpty(itemtypestring)) + { + argstart = itemtypestring.IndexOf(",") + 1; + } + + if (argstart > 1 && argstart < itemtypestring.Length) + { + typeargs = ParseCommaArgs(itemtypestring.Substring(argstart), 15); + } + return typeargs; + + } + + return null; + } + + // take a string of the form str-opendelim-str-closedelim-str-closedelimstr + public static string[] ParseToMatchingParen(string str, char opendelim, char closedelim) + { + var nopen = 1; + var nclose = 0; + var splitpoint = str.Length; + for (var i = 0; i < str.Length; i++) + { + // walk through the string until a matching close delimstr is found + if (str[i] == opendelim) + { + nopen++; + } + + if (str[i] == closedelim) + { + nclose++; + } + + if (nopen == nclose) + { + splitpoint = i; + break; + } + } + + var args = new string[2]; + + // allow missing closing delimiters at the end of the line, basically just treat eol as a closing delim + + args[0] = str.Substring(0, splitpoint); + args[1] = ""; + if (splitpoint + 1 < str.Length) + { + args[1] = str.Substring(splitpoint + 1, str.Length - splitpoint - 1); + } + + return args; + } + + public static string[] ParseString(string str, int nitems, string delimstr) + { + if (str == null || delimstr == null) + { + return null; + } + + var delims = delimstr.ToCharArray(); + str = str.Trim(); + var args = str.Split(delims, nitems); + + return args; + } + + public static string[] ParseSlashArgs(string str, int nitems) + { + if (str == null) + { + return null; + } + + str = str.Trim(); + + string[] args; + // this supports strings that may have special html formatting in them that use the / + if (str.Contains("")) + { + // or use indexof to do it with more context control + var tmparray = new List(); + // find the next slash char + var index = 0; + var preindex = 0; + var searchindex = 0; + var length = str.Length; + while (index >= 0 && searchindex < length && tmparray.Count < nitems - 1) + { + index = str.IndexOf('/', searchindex); + + if (index >= 0) + { + // check the char before it and after it to ignore + if (index > 0 && str[index - 1] == '<' || index < length - 1 && str[index + 1] == '>') + { + // skip it + searchindex = index + 1; + } + else + { + // split it + tmparray.Add(str.Substring(preindex, index - preindex)); + + preindex = index + 1; + searchindex = preindex; + } + } + + } + + // is there still room for more args? + if (tmparray.Count <= nitems - 1 && preindex < length) + { + // searched past the end and didnt find anything + tmparray.Add(str.Substring(preindex, length - preindex)); + } + + // turn tmparray into a string[] + + args = new string[tmparray.Count]; + tmparray.CopyTo(args); + } + else + { + // just use split to do it with no context control + args = str.Split(slashdelim, nitems); + + } + + return args; + } + + public static string[] ParseCommaArgs(string str, int nitems) + { + if (str == null) + { + return null; + } + + str = str.Trim(); + + var args = str.Split(commadelim, nitems); + return args; + } + + public static string[] ParseLiteralTerminator(string str) + { + if (str == null) + { + return null; + } + + str = str.Trim(); + + var args = str.Split(literalend, 2); + return args; + } + + public static string[] ParseSemicolonArgs(string str, int nitems) + { + if (str == null) + { + return null; + } + + str = str.Trim(); + + var args = str.Split(semicolondelim, nitems); + return args; + } + + public static string[] SplitString(string str, string separator) + { + if (str == null || separator == null) + { + return null; + } + + var lastindex = 0; + var strargs = new List(); + while (true) + { + // go through the string and find the first instance of the separator + var index = str.IndexOf(separator); + if (index < 0) + { + // no separator so its the end of the string + strargs.Add(str); + break; + } + + var arg = str.Substring(lastindex, index); + + strargs.Add(arg); + + str = str.Substring(index + separator.Length, str.Length - (index + separator.Length)); + } + + // now make the string args + var args = new string[strargs.Count]; + for (var i = 0; i < strargs.Count; i++) + { + args[i] = strargs[i]; + } + + return args; + } + + public static void AddSpawnItem(XmlSpawner spawner, object invoker, XmlSpawner.SpawnObject theSpawn, Item item, Point3D location, Map map, Mobile trigmob, bool requiresurface, + string propertyString, out string status_str) + { + AddSpawnItem(spawner, invoker, theSpawn, item, location, map, trigmob, requiresurface, null, propertyString, false, out status_str); + } + + public static void AddSpawnItem(XmlSpawner spawner, XmlSpawner.SpawnObject theSpawn, Item item, Point3D location, Map map, Mobile trigmob, bool requiresurface, + List spawnpositioning, string propertyString, bool smartspawn, out string status_str) + { + AddSpawnItem(spawner, spawner, theSpawn, item, location, map, trigmob, requiresurface, spawnpositioning, propertyString, smartspawn, out status_str); + } + + public static void AddSpawnItem(XmlSpawner spawner, object invoker, XmlSpawner.SpawnObject theSpawn, Item item, Point3D location, Map map, Mobile trigmob, bool requiresurface, + List spawnpositioning, string propertyString, bool smartspawn, out string status_str) + { + status_str = null; + if (item == null || theSpawn == null) + { + return; + } + + // add the item to the spawned list + theSpawn.SpawnedObjects.Add(item); + + item.Spawner = spawner; + + if (spawner != null) + { + // this is being called by a spawner so use spawner information for placement + if (!spawner.Deleted) + { + // set the item amount + if (spawner.StackAmount > 1 && item.Stackable) + { + item.Amount = spawner.StackAmount; + } + // if this is in any container such as a pack then add to the container. + if (spawner.Parent is Container parent) + { + var loc = spawner.Location; + + if (!smartspawn) + { + item.OnBeforeSpawn(loc, map); + } + + item.Location = loc; + + // check to see whether we drop or add the item based on the spawnrange + // this will distribute multiple items around the spawn point, and allow precise + // placement of single spawns at the spawn point + if (spawner.SpawnRange > 0) + { + parent.DropItem(item); + } + else + { + parent.AddItem(item); + } + } + else + { + // if the spawn entry is in a subgroup and has a packrange, then get the packcoord + + var packcoord = Point3D.Zero; + if (theSpawn.PackRange >= 0 && theSpawn.SubGroup > 0) + { + packcoord = spawner.GetPackCoord(theSpawn.SubGroup); + } + var loc = spawner.GetSpawnPosition(requiresurface, theSpawn.PackRange, packcoord, spawnpositioning); + + if (!smartspawn) + { + item.OnBeforeSpawn(loc, map); + } + + // standard placement for all items in the world + item.MoveToWorld(loc, map); + } + } + else + { + // if the spawner has already been deleted then delete the item since it cannot be cleaned up by spawner deletion any longer + item.Delete(); + return; + } + } + else + { + if (!smartspawn) + { + item.OnBeforeSpawn(location, map); + } + // use the location and map info passed in + // this allows AddSpawnItem to be called by objects other than spawners as long as they pass in a valid SpawnObject + item.MoveToWorld(location, map); + } + + // clear the taken flag on all newly spawned items + ItemFlags.SetTaken(item, false); + + if (!smartspawn) + { + item.OnAfterSpawn(); + } + + // apply the parsed arguments from the typestring using setcommand + // be sure to do this after setting map and location so that errors dont place the mob on the internal map + _ = ApplyObjectStringProperties(spawner, propertyString, item, trigmob, spawner, out status_str); + } + + public static bool SpawnTypeKeyword(object invoker, XmlSpawner.SpawnObject TheSpawn, string typeName, string substitutedtypeName, + Mobile triggermob, Map map, out string status_str) + { + return SpawnTypeKeyword(invoker, TheSpawn, typeName, substitutedtypeName, + triggermob, map, out status_str, 0); + } + + public static bool SpawnTypeKeyword(object invoker, XmlSpawner.SpawnObject TheSpawn, string typeName, string substitutedtypeName, Mobile triggermob, Map map, out string status_str, byte loops) + { + status_str = null; + + if (typeName == null || TheSpawn == null || substitutedtypeName == null) + { + return false; + } + + var spawner = invoker as XmlSpawner; + + // check for any special keywords that might appear in the type such as SET, GIVE, or TAKE + if (IsTypeKeyword(typeName)) + { + var kw = typeKeywordHash[typeName]; + + switch (kw) + { + case TypeKeyword.SET: + { + // the syntax is SET/prop/value/prop2/value... + // check for the SET,itemname or serialno[,itemtype]/prop/value form is used + var arglist = ParseSlashArgs(substitutedtypeName, 3); + var keywordargs = ParseString(arglist[0], 3, ","); + + if (keywordargs.Length > 1) + { + string typestr = null; + if (keywordargs.Length > 2) + { + typestr = keywordargs[2]; + } + + // is the itemname a serialno? + object setitem = null; + if (keywordargs[1].StartsWith("0x")) + { + uint serial; + try + { + serial = Convert.ToUInt32(keywordargs[1], 16); + setitem = World.FindEntity((Serial)serial); + } + catch { } + } + else + { + // just look it up by name + setitem = FindItemByName(spawner, keywordargs[1], typestr); + } + + if (setitem == null) + { + status_str = $"cant find unique item :{keywordargs[1]}"; + return false; + } + + _ = ApplyObjectStringProperties(spawner, substitutedtypeName, setitem, triggermob, invoker, out status_str); + } + else if (spawner != null) + { + _ = ApplyObjectStringProperties(spawner, substitutedtypeName, spawner.SetItem, triggermob, invoker, out status_str); + } + + TheSpawn.SpawnedObjects.Add(new KeywordTag(substitutedtypeName, spawner)); + + break; + } + case TypeKeyword.DESPAWN: + { + // the syntax is DESPAWN[,spawnername],subgroup + + // first find the spawner and group + var subgroup = -1; + var arglist = ParseSlashArgs(substitutedtypeName, 3); + var targetspawner = spawner; + if (arglist.Length > 0) + { + var keywordargs = ParseString(arglist[0], 3, ","); + if (keywordargs.Length < 2) + { + status_str = "missing subgroup in DESPAWN"; + return false; + } + + var subgroupstr = keywordargs[1]; + string spawnerstr = null; + if (keywordargs.Length > 2) + { + spawnerstr = keywordargs[1]; + subgroupstr = keywordargs[2]; + } + if (spawnerstr != null) + { + targetspawner = FindSpawnerByName(spawner, spawnerstr); + } + if (!int.TryParse(subgroupstr, out subgroup)) + { + subgroup = -1; + } + } + if (subgroup == -1) + { + status_str = "invalid subgroup in DESPAWN"; + return false; + } + + if (targetspawner != null) + { + targetspawner.ClearSubgroup(subgroup); + } + else + { + status_str = "invalid spawner in DESPAWN"; + return false; + } + + TheSpawn.SpawnedObjects.Add(new KeywordTag(substitutedtypeName, spawner)); + + break; + } + case TypeKeyword.SPAWN: + { + // the syntax is SPAWN[,spawnername],subgroup + + // first find the spawner and group + var subgroup = -1; + var arglist = ParseSlashArgs(substitutedtypeName, 3); + var targetspawner = spawner; + if (arglist.Length > 0) + { + var keywordargs = ParseString(arglist[0], 3, ","); + if (keywordargs.Length < 2) + { + status_str = "missing subgroup in SPAWN"; + return false; + } + + var subgroupstr = keywordargs[1]; + string spawnerstr = null; + if (keywordargs.Length > 2) + { + spawnerstr = keywordargs[1]; + subgroupstr = keywordargs[2]; + } + if (spawnerstr != null) + { + targetspawner = FindSpawnerByName(spawner, spawnerstr); + } + if (!int.TryParse(subgroupstr, out subgroup)) + { + subgroup = -1; + } + } + if (subgroup == -1) + { + status_str = "invalid subgroup in SPAWN"; + return false; + } + + if (targetspawner != null) + { + if (spawner != targetspawner) + { + // allow spawning of other spawners to be forced and ignore the normal loop protection + if (loops >= XmlSpawner.MaxLoops) //preventing looping from spawner to spawner, via recursive linked method calls + { + status_str = "recursive looping stop in SPAWN"; + return false; + } + _ = targetspawner.SpawnSubGroup(subgroup, false, true, (byte)(loops + 1)); + } + else + { + if (loops >= XmlSpawner.MaxLoops) + { + status_str = "recursive looping stop in SPAWN"; + return false; + } + _ = targetspawner.SpawnSubGroup(subgroup, (byte)(loops + 1)); + } + } + else + { + status_str = "invalid spawner in SPAWN"; + return false; + } + + TheSpawn.SpawnedObjects.Add(new KeywordTag(substitutedtypeName, spawner)); + + break; + } + case TypeKeyword.GOTO: + { + // the syntax is GOTO/subgroup + var arglist = ParseSlashArgs(substitutedtypeName, 3); + var group = -1; + if (arglist.Length < 2) + { + status_str = "insufficient args to GOTO"; + } + else + { + if (!int.TryParse(arglist[1], out group)) + { + status_str = "invalid subgroup arg to GOTO"; + group = -1; + } + } + if (status_str != null) + { + return false; + } + + // move the sequence to the specified subgroup + if (group >= 0 && spawner != null && !spawner.Deleted) + { + // note, this will activate sequential spawning if it wasnt already set + spawner.SequentialSpawn = group; + + // and suppress sequential advancement so that the specified group is the next to spawn + spawner.HoldSequence = true; + } + + TheSpawn.SpawnedObjects.Add(new KeywordTag(substitutedtypeName, spawner, 2)); + + break; + } + case TypeKeyword.COMMAND: + { + // the syntax is COMMAND/commandstring + var arglist = ParseSlashArgs(substitutedtypeName, 3); + if (arglist.Length > 0) + { + // mod to use a dummy char to issue commands + if (CommandMobileName != null) + { + var dummy = FindMobileByName(spawner, CommandMobileName, "Mobile"); + if (dummy != null) + { + _ = CommandSystem.Handle(dummy, $"{CommandSystem.Prefix}{arglist[1]}"); + } + } + else + if (triggermob != null && !triggermob.Deleted) + { + _ = CommandSystem.Handle(triggermob, $"{CommandSystem.Prefix}{arglist[1]}"); + } + } + else + { + status_str = "insufficient args to COMMAND"; + } + + TheSpawn.SpawnedObjects.Add(new KeywordTag(substitutedtypeName, spawner)); + + break; + } + default: + { + status_str = "unrecognized keyword"; + // should never get here + break; + } + } + // indicate successful keyword spawn + return true; + } + + // should never get here + status_str = "unrecognized keyword"; + return false; + } + + public static List GetItems(Region r) + { + var list = new List(); + if (r == null) + { + return list; + } + + var sectors = r.Sectors; + + if (sectors != null) + { + for (var i = 0; i < sectors.Length; i++) + { + var sector = sectors[i]; + + foreach (var item in sector.Items) + { + if (Region.Find(item.Location, item.Map).IsPartOf(r)) + { + list.Add(item); + } + } + } + } + + return list; + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/ExceptionLogging.cs b/Projects/UOContent/Engines/XMLSpawner/ExceptionLogging.cs new file mode 100644 index 000000000..a60a4017d --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/ExceptionLogging.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; + +namespace Server.Diagnostics; + +// TODO: Replace this with serilog +public class ExceptionLogging +{ + public static string LogDirectory { get; set; } + + private static StreamWriter _Output; + + public static StreamWriter Output + { + get + { + if (_Output == null) + { + _Output = new StreamWriter(Path.Combine(LogDirectory, $"{Core.Now.ToLongDateString()}.log"), true) + { + AutoFlush = true + }; + + _Output.WriteLine("##############################"); + _Output.WriteLine("Exception log started on {0}", Core.Now); + _Output.WriteLine(); + } + + return _Output; + } + } + + static ExceptionLogging() + { + var directory = Path.Combine(Core.BaseDirectory, "Logs/Exceptions"); + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + LogDirectory = directory; + } + + public static void LogException(Exception e) + { + Utility.PushColor(ConsoleColor.Red); + Console.WriteLine("Caught Exception:"); + Utility.PopColor(); + + Utility.PushColor(ConsoleColor.DarkRed); + Console.WriteLine(e); + Utility.PopColor(); + + Output.WriteLine("Exception Caught: {0}", Core.Now); + Output.WriteLine(e); + Output.WriteLine(); + } + + public static void LogException(Exception e, string arg) + { + Utility.PushColor(ConsoleColor.Red); + Console.WriteLine("Caught Exception: {0}", arg); + Utility.PopColor(); + + Utility.PushColor(ConsoleColor.DarkRed); + Console.WriteLine(e); + Utility.PopColor(); + + Output.WriteLine("Exception Caught: {0}", Core.Now); + Output.WriteLine(e); + Output.WriteLine(); + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/ItemFlags.cs b/Projects/UOContent/Engines/XMLSpawner/ItemFlags.cs new file mode 100644 index 000000000..802ed50cf --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/ItemFlags.cs @@ -0,0 +1,134 @@ +using System; +using Server.Targeting; + +namespace Server.Items; + +public partial class ItemFlags +{ + private const int StealableFlag = 0x00200000; + private const int TakenFlag = 0x00100000; + + public static void SetStealable(Item target, bool value) + { + target?.SetSavedFlag(StealableFlag, value); + } + public static bool GetStealable(Item target) => target != null && target.GetSavedFlag(StealableFlag); + + public static void SetTaken(Item target, bool value) + { + target?.SetSavedFlag(TakenFlag, value); + } + public static bool GetTaken(Item target) => target != null && target.GetSavedFlag(TakenFlag); + + [Usage("Flag flagfield")] + [Description("Gets the state of the specified SavedFlag on any item")] + public static void GetFlag_OnCommand(CommandEventArgs e) + { + var flag=0; + var error = false; + if (e.Arguments.Length > 0) + { + if (e.Arguments[0].StartsWith("0x")) + { + try{flag = Convert.ToInt32(e.Arguments[0].Substring(2), 16); } catch { error = true;} + } else + { + try{flag = int.Parse(e.Arguments[0]); } catch { error = true;} + } + + } + if (!error) + { + e.Mobile.Target = new GetFlagTarget(e,flag); + } else + { + try{ + e.Mobile.SendMessage(33,"Flag: Bad flagfield argument"); + } catch {} + } + } + + private class GetFlagTarget : Target + { + private CommandEventArgs m_e; + private int m_flag; + + public GetFlagTarget(CommandEventArgs e, int flag) : base (30, false, TargetFlags.None) + { + m_e = e; + m_flag = flag; + } + protected override void OnTarget(Mobile from, object targeted) + { + if (targeted is Item item) + { + var state = item.GetSavedFlag(m_flag); + + from.SendMessage($"Flag (0x{m_flag:X}) = {state}"); + } else + { + from.SendMessage("Must target an Item"); + } + } + } + + + [Usage("Stealable [true/false]")] + [Description("Sets/gets the stealable flag on any item")] + public static void SetStealable_OnCommand(CommandEventArgs e) + { + var state = false; + var error = false; + if (e.Arguments.Length > 0) + { + try + { + state = bool.Parse(e.Arguments[0]); + } + catch + { + error = true; + } + } + if (!error) + { + e.Mobile.Target = new SetStealableTarget(e, state); + } + + } + + private class SetStealableTarget : Target + { + private CommandEventArgs m_e; + private bool m_state; + private bool set; + + public SetStealableTarget(CommandEventArgs e, bool state) : base (30, false, TargetFlags.None) + { + m_e = e; + m_state = state; + if (e.Arguments.Length > 0) + { + set = true; + } + } + protected override void OnTarget(Mobile from, object targeted) + { + if (targeted is Item item) + { + if (set) + { + SetStealable(item, m_state); + } + + var state = GetStealable(item); + + from.SendMessage($"Stealable = {state}"); + + } else + { + from.SendMessage("Must target an Item"); + } + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/SpawnerExporter.cs b/Projects/UOContent/Engines/XMLSpawner/SpawnerExporter.cs new file mode 100644 index 000000000..a99f9cb44 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/SpawnerExporter.cs @@ -0,0 +1,283 @@ +using Server.Commands.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Server.Engines.Spawners; + +/* +** Sno's distro spawner importer/exporter +** +** [exportspawner filename - Saves distro spawners to XML to 'Saves/Spawners/filename' +** +** [importspawner filename - Restores distro spawners from 'Saves/Spawners/filename'. Note, this command does not check for +** duplication, so if you run it more than once you will end up with multiple spawners. +** +** These spawns can also be imported back in as XmlSpawners by using the '[xmlimportspawners Saves/Spawners/filename' command. +*/ + +namespace Server.Mobiles; + +public class SpawnerExporter +{ + public static void Initialize() + { + TargetCommands.Register(new ExportSpawnerCommand()); + CommandSystem.Register("ImportSpawners", AccessLevel.Administrator, ImportSpawners_OnCommand); + } + + public class ExportSpawnerCommand : BaseCommand + { + public ExportSpawnerCommand() + { + AccessLevel = AccessLevel.Administrator; + Supports = CommandSupport.Area | CommandSupport.Region | CommandSupport.Global | CommandSupport.Multi | CommandSupport.Single; + Commands = new[] { "ExportSpawner" }; + ObjectTypes = ObjectTypes.Items; + Usage = "ExportSpawner "; + Description = "Exports all Spawner objects to the specified filename."; + ListOptimized = true; + } + + public override void ExecuteList(CommandEventArgs e, List list) + { + var filename = e.GetString(0); + + var spawners = new ArrayList(); + + for (var i = 0; i < list.Count; ++i) + { + if (list[i] is Spawner) + { + var spawner = (Spawner)list[i]; + if (!spawner.Deleted && spawner.Map != Map.Internal && spawner.Parent == null) + { + spawners.Add(spawner); + } + } + } + + AddResponse($"{spawners.Count.ToString()} spawners exported to Saves/Spawners/{filename}."); + + ExportSpawners(spawners, filename); + } + + public override bool ValidateArgs(BaseCommandImplementor impl, CommandEventArgs e) + { + if (e.Arguments.Length >= 1) + { + return true; + } + + e.Mobile.SendMessage($"Usage: {Usage}"); + return false; + } + + private void ExportSpawners(ArrayList spawners, string filename) + { + if (spawners.Count == 0) + { + return; + } + + if (!Directory.Exists("Saves/Spawners")) + { + Directory.CreateDirectory("Saves/Spawners"); + } + + var filePath = Path.Combine("Saves/Spawners", filename); + + using (var op = new StreamWriter(filePath)) + { + var xml = new XmlTextWriter(op) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + + xml.WriteStartDocument(true); + + xml.WriteStartElement("spawners"); + + xml.WriteAttributeString("count", spawners.Count.ToString()); + + foreach (Spawner spawner in spawners) + { + ExportSpawner(spawner, xml); + } + + xml.WriteEndElement(); + + xml.Close(); + } + } + + private void ExportSpawner(Spawner spawner, XmlWriter xml) + { + xml.WriteStartElement("spawner"); + + xml.WriteStartElement("count"); + xml.WriteString(spawner.Count.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("group"); + xml.WriteString(spawner.Group.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("homerange"); + xml.WriteString(spawner.HomeRange.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("walkingrange"); + xml.WriteString(spawner.WalkingRange.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("maxdelay"); + xml.WriteString(spawner.MaxDelay.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("mindelay"); + xml.WriteString(spawner.MinDelay.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("team"); + xml.WriteString(spawner.Team.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("creaturesname"); + foreach (var entry in spawner.Entries) + { + xml.WriteStartElement("creaturename"); + xml.WriteString(entry.SpawnedName); + xml.WriteEndElement(); + } + xml.WriteEndElement(); + + // Item properties + + xml.WriteStartElement("name"); + xml.WriteString(spawner.Name); + xml.WriteEndElement(); + + xml.WriteStartElement("location"); + xml.WriteString(spawner.Location.ToString()); + xml.WriteEndElement(); + + xml.WriteStartElement("map"); + xml.WriteString(spawner.Map.ToString()); + xml.WriteEndElement(); + + xml.WriteEndElement(); + } + } + + [Usage("ImportSpawners")] + [Description("Recreates Spawner items from the specified file.")] + public static void ImportSpawners_OnCommand(CommandEventArgs e) + { + if (e.Arguments.Length >= 1) + { + var filename = e.GetString(0); + var filePath = Path.Combine("Saves/Spawners", filename); + + if (File.Exists(filePath)) + { + var doc = new XmlDocument(); + doc.Load(filePath); + + var root = doc["spawners"]; + + int successes = 0, failures = 0; + + foreach (XmlElement spawner in root.GetElementsByTagName("spawner")) + { + try + { + ImportSpawner(spawner); + successes++; + } + catch (Exception ex) + { + failures++; + Diagnostics.ExceptionLogging.LogException(ex); + } + } + + e.Mobile.SendMessage($"{successes} spawners loaded successfully from {filePath}, {failures} failures."); + } + else + { + e.Mobile.SendMessage($"File {filePath} does not exist."); + } + } + else + { + e.Mobile.SendMessage("Usage: [ImportSpawners "); + } + } + + private static string GetText(XmlNode node, string defaultValue) + { + if (node == null) + { + return defaultValue; + } + + return node.InnerText; + } + + private static void ImportSpawner(XmlNode node) + { + var count = int.Parse(GetText(node["count"], "1")); + var homeRange = int.Parse(GetText(node["homerange"], "4")); + + var walkingRange = int.Parse(GetText(node["walkingrange"], "-1")); + + var team = int.Parse(GetText(node["team"], "0")); + + var group = bool.Parse(GetText(node["group"], "False")); + var maxDelay = TimeSpan.Parse(GetText(node["maxdelay"], "10:00")); + var minDelay = TimeSpan.Parse(GetText(node["mindelay"], "05:00")); + var creaturesName = LoadCreaturesName(node["creaturesname"]); + + var name = GetText(node["name"], "Spawner"); + var location = Point3D.Parse(GetText(node["location"], "Error")); + var map = Map.Parse(GetText(node["map"], "Error")); + + var spawner = new Spawner(count, minDelay, maxDelay, team, homeRange, creaturesName.ToArray()); + if (walkingRange >= 0) + { + spawner.WalkingRange = walkingRange; + } + + spawner.Name = name; + spawner.MoveToWorld(location, map); + if (spawner.Map == Map.Internal) + { + spawner.Delete(); + throw new Exception("Spawner created on Internal map."); + } + spawner.Respawn(); + } + + private static IEnumerable LoadCreaturesName(XmlElement node) + { + var names = new List(); + + if (node != null) + { + foreach (XmlElement ele in node.GetElementsByTagName("creaturename")) + { + if (ele != null) + { + names.Add(ele.InnerText); + } + } + } + + return names; + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlPropsGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlPropsGump.cs new file mode 100644 index 000000000..aa7c4c066 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlPropsGump.cs @@ -0,0 +1,715 @@ +using Server.Commands.Generic; +using Server.Network; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using CPA = Server.CommandPropertyAttribute; +/* +** modified properties gumps taken from RC0 properties gump scripts to support the special XmlSpawner properties gump +*/ + +namespace Server.Gumps; + +public class XmlPropertiesGump : Gump +{ + private readonly ArrayList m_List; + private int m_Page; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int NameWidth = 103; + private static readonly int ValueWidth = 82; + + private static readonly int EntryCount = 66; + private static readonly int ColumnEntryCount = 22; + + private static readonly int TypeWidth = NameWidth + OffsetSize + ValueWidth; + + private static readonly int TotalWidth = OffsetSize + NameWidth + OffsetSize + ValueWidth + OffsetSize + SetWidth + OffsetSize; + + public XmlPropertiesGump(Mobile mobile, object o) : base(GumpOffsetX, GumpOffsetY) + { + m_Mobile = mobile; + m_Object = o; + m_List = BuildList(); + + Initialize(0); + } + + public XmlPropertiesGump(Mobile mobile, object o, Stack stack, StackEntry parent) : base(GumpOffsetX, GumpOffsetY) + { + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_List = BuildList(); + + if (parent != null) + { + if (m_Stack == null) + { + m_Stack = new Stack(); + } + + m_Stack.Push(parent); + } + + Initialize(0); + } + + public XmlPropertiesGump(Mobile mobile, object o, Stack stack, ArrayList list, int page) : base(GumpOffsetX, GumpOffsetY) + { + m_Mobile = mobile; + m_Object = o; + m_List = list; + m_Stack = stack; + + Initialize(page); + } + + private void Initialize(int page) + { + m_Page = page; + + var count = m_List.Count - page * EntryCount; + + if (count < 0) + { + count = 0; + } + else if (count > EntryCount) + { + count = EntryCount; + } + + var lastIndex = page * EntryCount + count - 1; + + if (lastIndex >= 0 && lastIndex < m_List.Count && m_List[lastIndex] == null) + { + --count; + } + + var totalHeight = OffsetSize + (EntryHeight + OffsetSize) * (ColumnEntryCount + 1); + + AddPage(0); + + AddBackground(0, 0, TotalWidth * 3 + BorderSize * 2, BorderSize + totalHeight + BorderSize, BackGumpID); + AddImageTiled(BorderSize, BorderSize + EntryHeight, (TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0)) * 3, totalHeight - EntryHeight, OffsetGumpID); + + var x = BorderSize + OffsetSize; + var y = BorderSize; + + if (m_Object is Item item) + { + AddLabelCropped(x + TextOffsetX, y, TypeWidth - TextOffsetX, EntryHeight, TextHue, item.Name); + } + + var propcount = 0; + for (int i = 0, index = page * EntryCount; i <= count && index < m_List.Count; ++i, ++index) + { + // do the multi column display + var column = propcount / ColumnEntryCount; + if (propcount % ColumnEntryCount == 0) + { + y = BorderSize; + } + + x = BorderSize + OffsetSize + column * (ValueWidth + NameWidth + OffsetSize * 2 + SetOffsetX + SetWidth); + y += EntryHeight + OffsetSize; + + var o = m_List[index]; + + if (o == null) + { + AddImageTiled(x - OffsetSize, y, TotalWidth, EntryHeight, BackGumpID + 4); + propcount++; + } + else if (o is PropertyInfo prop) + { + propcount++; + + // look for the default value of the equivalent property in the XmlSpawnerDefaults.DefaultEntry class + + var huemodifier = TextHue; + var de = new Mobiles.XmlSpawnerDefaults.DefaultEntry(); + var ftype = de.GetType(); + + var finfo = ftype.GetField(prop.Name); + + // is there an equivalent default field? + if (finfo != null) + { + // see if the value is different from the default + if (ValueToString(finfo.GetValue(de)) != ValueToString(prop)) + { + huemodifier = 68; + } + } + + AddImageTiled(x, y, NameWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, NameWidth - TextOffsetX, EntryHeight, huemodifier, prop.Name); + x += NameWidth + OffsetSize; + AddImageTiled(x, y, ValueWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, ValueWidth - TextOffsetX, EntryHeight, huemodifier, ValueToString(prop)); + x += ValueWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + var cpa = GetCPA(prop); + + if (prop.CanWrite && cpa != null && m_Mobile.AccessLevel >= cpa.WriteLevel) + { + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, i + 3); + } + } + } + } + + public static string[] m_BoolNames = { "True", "False" }; + public static object[] m_BoolValues = { true, false }; + + public static string[] m_PoisonNames = { "None", "Lesser", "Regular", "Greater", "Deadly", "Lethal" }; + public static object[] m_PoisonValues = { null, Poison.Lesser, Poison.Regular, Poison.Greater, Poison.Deadly, Poison.Lethal }; + + public override void OnResponse(NetState state, 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 != null && m_Stack.Count > 0) + { + var entry = m_Stack.Pop(); + from.SendGump(new XmlPropertiesGump(from, entry.m_Object, m_Stack, null)); + } + + break; + } + case 1: // Previous + { + if (m_Page > 0) + { + from.SendGump(new XmlPropertiesGump(from, m_Object, m_Stack, m_List, m_Page - 1)); + } + + break; + } + case 2: // Next + { + if ((m_Page + 1) * EntryCount < m_List.Count) + { + from.SendGump(new XmlPropertiesGump(from, m_Object, m_Stack, m_List, m_Page + 1)); + } + + break; + } + default: + { + var index = m_Page * EntryCount + (info.ButtonID - 3); + + if (index >= 0 && index < m_List.Count) + { + var prop = m_List[index] as PropertyInfo; + + if (prop == null) + { + return; + } + + var attr = GetCPA(prop); + + if (!prop.CanWrite || attr == null || from.AccessLevel < attr.WriteLevel) + { + return; + } + + var type = prop.PropertyType; + + if (IsType(type, typeofMobile) || IsType(type, typeofItem)) + { + from.SendGump(new XmlSetObjectGump(prop, from, m_Object, m_Stack, type, m_Page, m_List)); + } + else if (IsType(type, typeofType)) + { + from.Target = new XmlSetObjectTarget(prop, from, m_Object, m_Stack, type, m_Page, m_List); + } + else if (IsType(type, typeofPoint3D)) + { + from.SendGump(new XmlSetPoint3DGump(prop, from, m_Object, m_Stack, m_Page, m_List)); + } + else if (IsType(type, typeofPoint2D)) + { + from.SendGump(new XmlSetPoint2DGump(prop, from, m_Object, m_Stack, m_Page, m_List)); + } + else if (IsType(type, typeofTimeSpan)) + { + from.SendGump(new XmlSetTimeSpanGump(prop, from, m_Object, m_Stack, m_Page, m_List)); + } + else if (IsCustomEnum(type)) + { + from.SendGump(new XmlSetCustomEnumGump(prop, from, m_Object, m_Stack, m_Page, m_List, GetCustomEnumNames(type))); + } + else if (IsType(type, typeofEnum)) + { + from.SendGump(new XmlSetListOptionGump(prop, from, m_Object, m_Stack, m_Page, m_List, Enum.GetNames(type), GetObjects(Enum.GetValues(type)))); + } + else if (IsType(type, typeofBool)) + { + from.SendGump(new XmlSetListOptionGump(prop, from, m_Object, m_Stack, m_Page, m_List, m_BoolNames, m_BoolValues)); + } + else if (IsType(type, typeofString) || IsType(type, typeofReal) || IsType(type, typeofNumeric)) + { + from.SendGump(new XmlSetGump(prop, from, m_Object, m_Stack, m_Page, m_List)); + } + else if (IsType(type, typeofPoison)) + { + from.SendGump(new XmlSetListOptionGump(prop, from, m_Object, m_Stack, m_Page, m_List, m_PoisonNames, m_PoisonValues)); + } + else if (IsType(type, typeofMap)) + { + from.SendGump(new XmlSetListOptionGump(prop, from, m_Object, m_Stack, m_Page, m_List, Map.GetMapNames(), Map.GetMapValues())); + } + else if (IsType(type, typeofSkills) && m_Object is Mobile mobile) + { + from.SendGump(new XmlPropertiesGump(from, mobile, m_Stack, m_List, m_Page)); + from.SendGump(new SkillsGump(from, mobile)); + } + else if (HasAttribute(type, typeofPropertyObject, true)) + { + var obj = prop.GetValue(m_Object, null); + + from.SendGump(obj != null + ? new XmlPropertiesGump(from, obj, m_Stack, + new StackEntry(m_Object, prop)) + : new XmlPropertiesGump(from, m_Object, m_Stack, m_List, m_Page)); + } + } + + break; + } + } + } + + 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(typeofCustomEnum, false); + + private static string[] GetCustomEnumNames(Type type) + { + var attrs = type.GetCustomAttributes(typeofCustomEnum, false); + + if (attrs.Length == 0) + { + return new string[0]; + } + + var ce = attrs[0] as CustomEnumAttribute; + + if (ce == null) + { + return new string[0]; + } + + return ce.Names; + } + + private static bool HasAttribute(Type type, Type check, bool inherit) + { + var objs = type.GetCustomAttributes(check, inherit); + + return objs.Length > 0; + } + + private static bool IsType(Type type, Type check) => type == check || type.IsSubclassOf(check); + + private static bool IsType(Type type, Type[] check) + { + for (var i = 0; i < check.Length; ++i) + { + if (IsType(type, check[i])) + { + return true; + } + } + + return false; + } + + private static readonly Type typeofMobile = typeof(Mobile); + private static readonly Type typeofItem = typeof(Item); + private static readonly Type typeofType = typeof(Type); + private static readonly Type typeofPoint3D = typeof(Point3D); + private static readonly Type typeofPoint2D = typeof(Point2D); + private static readonly Type typeofTimeSpan = typeof(TimeSpan); + private static readonly Type typeofCustomEnum = typeof(CustomEnumAttribute); + private static readonly Type typeofEnum = typeof(Enum); + private static readonly Type typeofBool = typeof(bool); + private static readonly Type typeofString = typeof(string); + private static readonly Type typeofPoison = typeof(Poison); + private static readonly Type typeofMap = typeof(Map); + private static readonly Type typeofSkills = typeof(Skills); + private static readonly Type typeofPropertyObject = typeof(PropertyObjectAttribute); + private static readonly Type typeofNoSort = typeof(NoSortAttribute); + + private static readonly Type[] typeofReal = + { + typeof(float), + typeof(double) + }; + + private static readonly Type[] typeofNumeric = + { + typeof(byte), + typeof(short), + typeof(int), + typeof(long), + typeof(sbyte), + typeof(ushort), + typeof(uint), + typeof(ulong) + }; + + 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()}!"; + } + } + + public static string ValueToString(object o) + { + if (o == null) + { + return "-null-"; + } + + if (o is string s1) + { + return $"\"{s1}\""; + } + + if (o is bool) + { + return o.ToString(); + } + + if (o is char c) + { + return $"0x{(int)c:X} '{c}'"; + } + + if (o is Serial s) + { + if (s.IsValid) + { + if (s.IsItem) + { + return $"(I) 0x{s.Value:X}"; + } + if (s.IsMobile) + { + return $"(M) 0x{s.Value:X}"; + } + } + + return $"(?) 0x{s.Value:X}"; + } + + if (o is byte || o is sbyte || o is short || o is ushort || o is int || o is uint || o is long || o is ulong) + { + return string.Format("{0} (0x{0:X})", o); + } + + if (o is double) + { + return o.ToString(); + } + + if (o is Mobile mobile) + { + return $"(M) 0x{mobile.Serial.Value:X} \"{mobile.Name}\""; + } + + if (o is Item item) + { + return $"(I) 0x{item.Serial:X}"; + } + + if (o is Type type) + { + return type.Name; + } + + return o.ToString(); + } + + private ArrayList BuildList() + { + var type = m_Object.GetType(); + + var props = type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); + + var groups = GetGroups(type, props); + var list = new ArrayList(); + + for (var i = 0; i < groups.Count; ++i) + { + var de = (DictionaryEntry)groups[i]; + var groupList = (ArrayList)de.Value; + + if (!HasAttribute((Type)de.Key, typeofNoSort, false)) + { + groupList.Sort(PropertySorter.Instance); + } + + if (i != 0) + { + list.Add(null); + } + + list.Add(de.Key); + list.AddRange(groupList); + } + + return list; + } + + private static readonly Type typeofCPA = typeof(CPA); + private static readonly Type typeofObject = typeof(object); + + private static CPA GetCPA(PropertyInfo prop) + { + var attrs = prop.GetCustomAttributes(typeofCPA, false); + + if (attrs.Length > 0) + { + return attrs[0] as CPA; + } + + return null; + } + + private ArrayList GetGroups(Type objectType, PropertyInfo[] props) + { + var groups = new Hashtable(); + + 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 == null || baseType == typeofObject) + { + break; + } + + if (baseType.GetProperty(prop.Name, prop.PropertyType) != null) + { + type = baseType; + } + else + { + break; + } + } + + var list = (ArrayList)groups[type]; + + if (list == null) + { + groups[type] = list = new ArrayList(); + } + + list.Add(prop); + } + } + } + + var sorted = new ArrayList(groups); + + sorted.Sort(new GroupComparer(objectType)); + + return sorted; + } + + public static object GetObjectFromString(Type t, string s) + { + if (t == typeof(string)) + { + return s; + } + + if (t == typeof(byte) || t == typeof(sbyte) || t == typeof(short) || t == typeof(ushort) || t == typeof(int) || t == typeof(uint) || t == typeof(long) || t == typeof(ulong)) + { + if (s.StartsWith("0x")) + { + if (t == typeof(ulong) || t == typeof(uint) || t == typeof(ushort) || t == typeof(byte)) + { + return Convert.ChangeType(Convert.ToUInt64(s[2..], 16), t); + } + + return Convert.ChangeType(Convert.ToInt64(s[2..], 16), t); + } + + return Convert.ChangeType(s, t); + } + + if (t == typeof(double) || t == typeof(float)) + { + return Convert.ChangeType(s, t); + } + + throw new Exception("bad"); + } + + private class PropertySorter : IComparer + { + public static readonly PropertySorter Instance = new(); + + private PropertySorter() + { + } + + public int Compare(object x, object y) + { + if (x == null && y == null) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + var a = x as PropertyInfo; + var b = y as PropertyInfo; + + if (a == null || b == null) + { + throw new ArgumentException(); + } + + return a.Name.CompareTo(b.Name); + } + } + + private class GroupComparer : IComparer + { + private readonly Type m_Start; + + public GroupComparer(Type start) => m_Start = start; + + private static readonly Type typeofObject = typeof(object); + + private int GetDistance(Type type) + { + var current = m_Start; + + int dist; + + for (dist = 0; current != null && current != typeofObject && current != type; ++dist) + { + current = current.BaseType; + } + + return dist; + } + + public int Compare(object x, object y) + { + if (x == null && y == null) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + if (!(x is DictionaryEntry de1) || !(y is DictionaryEntry de2)) + { + throw new ArgumentException(); + } + + var a = (Type)de1.Key; + var b = (Type)de2.Key; + + return GetDistance(a).CompareTo(GetDistance(b)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetCustomEnumGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetCustomEnumGump.cs new file mode 100644 index 000000000..bf545351a --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetCustomEnumGump.cs @@ -0,0 +1,47 @@ +using Server.Commands; +using Server.Network; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetCustomEnumGump : XmlSetListOptionGump +{ + private readonly string[] m_Names; + public XmlSetCustomEnumGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, int propspage, ArrayList list, string[] names) : base(prop, mobile, o, stack, propspage, list, names, null) + { + m_Names = names; + } + + public override void OnResponse(NetState sender, RelayInfo relayInfo) + { + var index = relayInfo.ButtonID - 1; + + if (index >= 0 && index < m_Names.Length) + { + try + { + var info = m_Property.PropertyType.GetMethod("Parse", new[] { typeof(string) }); + + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, m_Names[index]); + + if (info != null) + { + m_Property.SetValue(m_Object, info.Invoke(null, new object[] { m_Names[index] }), null); + } + else if (m_Property.PropertyType == typeof(Enum) || m_Property.PropertyType.IsSubclassOf(typeof(Enum))) + { + m_Property.SetValue(m_Object, Enum.Parse(m_Property.PropertyType, m_Names[index], false), null); + } + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetGump.cs new file mode 100644 index 000000000..73ad7c6a7 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetGump.cs @@ -0,0 +1,250 @@ +using Server.Commands; +using Server.HuePickers; +using Server.Network; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetGump : Gump +{ + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int EntryWidth = 212; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + private static readonly int TotalHeight = OffsetSize + 2 * (EntryHeight + OffsetSize); + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + private static readonly int BackHeight = BorderSize + TotalHeight + BorderSize; + + public XmlSetGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(GumpOffsetX, GumpOffsetY) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + + var canNull = !prop.PropertyType.IsValueType; + var canDye = prop.IsDefined(typeof(HueAttribute), false); + + var xextend = 0; + if (prop.PropertyType == typeof(string)) + { + xextend = 300; + } + + var val = prop.GetValue(m_Object, null); + + var initialText = val == null ? "" : val.ToString(); + + AddPage(0); + + AddBackground(0, 0, BackWidth + xextend, BackHeight + (canNull ? EntryHeight + OffsetSize : 0) + (canDye ? EntryHeight + OffsetSize : 0), BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth + xextend - (OldStyle ? SetWidth + OffsetSize : 0), TotalHeight + (canNull ? EntryHeight + OffsetSize : 0) + (canDye ? EntryHeight + OffsetSize : 0), OffsetGumpID); + + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize; + + AddImageTiled(x, y, EntryWidth + xextend, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth + xextend - TextOffsetX, EntryHeight, TextHue, prop.Name); + x += EntryWidth + xextend + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth + xextend, EntryHeight, EntryGumpID); + AddTextEntry(x + TextOffsetX, y, EntryWidth + xextend - TextOffsetX, EntryHeight, TextHue, 0, initialText); + x += EntryWidth + xextend + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 1); + + if (canNull) + { + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth + xextend, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth + xextend - TextOffsetX, EntryHeight, TextHue, "Null"); + x += EntryWidth + xextend + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 2); + } + + if (canDye) + { + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth + xextend, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth + xextend - TextOffsetX, EntryHeight, TextHue, "Hue Picker"); + x += EntryWidth + xextend + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 3); + } + } + + private class InternalPicker : HuePicker + { + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public InternalPicker(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(((IHued)o).HuedItemID) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + } + + public override void OnResponse(int hue) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, hue.ToString()); + m_Property.SetValue(m_Object, hue, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + object toSet; + bool shouldSet, shouldSend = true; + + switch (info.ButtonID) + { + case 1: + { + var text = info.GetTextEntry(0); + + if (text != null) + { + try + { + toSet = XmlPropertiesGump.GetObjectFromString(m_Property.PropertyType, text.Text); + shouldSet = true; + } + catch + { + toSet = null; + shouldSet = false; + m_Mobile.SendMessage("Bad format"); + } + } + else + { + toSet = null; + shouldSet = false; + } + + break; + } + case 2: // Null + { + toSet = null; + shouldSet = true; + + break; + } + case 3: // Hue Picker + { + toSet = null; + shouldSet = false; + shouldSend = false; + + m_Mobile.SendHuePicker(new InternalPicker(m_Property, m_Mobile, m_Object, m_Stack, m_Page, m_List)); + + break; + } + default: + { + toSet = null; + shouldSet = false; + + break; + } + } + + if (shouldSet) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet == null ? "(null)" : toSet.ToString()); + m_Property.SetValue(m_Object, toSet, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + if (shouldSend) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetListOptionGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetListOptionGump.cs new file mode 100644 index 000000000..7c40d47be --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetListOptionGump.cs @@ -0,0 +1,196 @@ +using Server.Commands; +using Server.Network; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetListOptionGump : Gump +{ + protected PropertyInfo m_Property; + protected Mobile m_Mobile; + protected object m_Object; + protected Stack m_Stack; + protected int m_Page; + protected ArrayList m_List; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int HeaderGumpID = PropsConfig.HeaderGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int PrevWidth = PropsConfig.PrevWidth; + public static readonly int PrevOffsetX = PropsConfig.PrevOffsetX, PrevOffsetY = PropsConfig.PrevOffsetY; + public static readonly int PrevButtonID1 = PropsConfig.PrevButtonID1; + public static readonly int PrevButtonID2 = PropsConfig.PrevButtonID2; + + public static readonly int NextWidth = PropsConfig.NextWidth; + public static readonly int NextOffsetX = PropsConfig.NextOffsetX, NextOffsetY = PropsConfig.NextOffsetY; + public static readonly int NextButtonID1 = PropsConfig.NextButtonID1; + public static readonly int NextButtonID2 = PropsConfig.NextButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int EntryWidth = 212; + private static readonly int EntryCount = 13; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + + private static readonly bool PrevLabel = OldStyle, NextLabel = OldStyle; + + private static readonly int PrevLabelOffsetX = PrevWidth + 1; + private static readonly int PrevLabelOffsetY = 0; + + private static readonly int NextLabelOffsetX = -29; + private static readonly int NextLabelOffsetY = 0; + + protected object[] m_Values; + + public XmlSetListOptionGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, int propspage, ArrayList list, string[] names, object[] values) : base(GumpOffsetX, GumpOffsetY) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = propspage; + m_List = list; + + m_Values = values; + + var pages = (names.Length + EntryCount - 1) / EntryCount; + var index = 0; + + for (var page = 1; page <= pages; ++page) + { + AddPage(page); + + var start = (page - 1) * EntryCount; + var count = names.Length - start; + + if (count > EntryCount) + { + count = EntryCount; + } + + var totalHeight = OffsetSize + (count + 2) * (EntryHeight + OffsetSize); + var backHeight = BorderSize + totalHeight + BorderSize; + + AddBackground(0, 0, BackWidth, backHeight, BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), totalHeight, OffsetGumpID); + + + + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize; + + var emptyWidth = TotalWidth - PrevWidth - NextWidth - OffsetSize * 4 - (OldStyle ? SetWidth + OffsetSize : 0); + + AddImageTiled(x, y, PrevWidth, EntryHeight, HeaderGumpID); + + if (page > 1) + { + AddButton(x + PrevOffsetX, y + PrevOffsetY, PrevButtonID1, PrevButtonID2, 0, GumpButtonType.Page, page - 1); + + if (PrevLabel) + { + AddLabel(x + PrevLabelOffsetX, y + PrevLabelOffsetY, TextHue, "Previous"); + } + } + + x += PrevWidth + OffsetSize; + + if (!OldStyle) + { + AddImageTiled(x - (OldStyle ? OffsetSize : 0), y, emptyWidth + (OldStyle ? OffsetSize * 2 : 0), EntryHeight, HeaderGumpID); + } + + x += emptyWidth + OffsetSize; + + if (!OldStyle) + { + AddImageTiled(x, y, NextWidth, EntryHeight, HeaderGumpID); + } + + if (page < pages) + { + AddButton(x + NextOffsetX, y + NextOffsetY, NextButtonID1, NextButtonID2, 0, GumpButtonType.Page, page + 1); + + if (NextLabel) + { + AddLabel(x + NextLabelOffsetX, y + NextLabelOffsetY, TextHue, "Next"); + } + } + + + + AddRect(0, prop.Name, 0); + + for (var i = 0; i < count; ++i) + { + AddRect(i + 1, names[index], ++index); + } + } + } + + private void AddRect(int index, string str, int button) + { + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize + (index + 1) * (EntryHeight + OffsetSize); + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, str); + + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + if (button != 0) + { + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, button); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + var index = info.ButtonID - 1; + + if (index >= 0 && index < m_Values.Length) + { + try + { + var toSet = m_Values[index]; + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet == null ? "(-null-)" : toSet.ToString()); + m_Property.SetValue(m_Object, toSet, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetObjectGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetObjectGump.cs new file mode 100644 index 000000000..d973cdf60 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetObjectGump.cs @@ -0,0 +1,302 @@ +using Server.Commands; +using Server.Commands.Generic; +using Server.Network; +using Server.Prompts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetObjectGump : Gump +{ + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly Type m_Type; + private readonly int m_Page; + private readonly ArrayList m_List; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int EntryWidth = 212; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + private static readonly int TotalHeight = OffsetSize + 5 * (EntryHeight + OffsetSize); + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + private static readonly int BackHeight = BorderSize + TotalHeight + BorderSize; + + public XmlSetObjectGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, Type type, int page, ArrayList list) : base(GumpOffsetX, GumpOffsetY) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Type = type; + m_Page = page; + m_List = list; + + var initialText = XmlPropertiesGump.ValueToString(o, prop); + + AddPage(0); + + AddBackground(0, 0, BackWidth, BackHeight, BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), TotalHeight, OffsetGumpID); + + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, prop.Name); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, initialText); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 1); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "Change by Serial"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 2); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "Nullify"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 3); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "View Properties"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 4); + } + + private class InternalPrompt : Prompt + { + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly Type m_Type; + private readonly int m_Page; + private readonly ArrayList m_List; + + public InternalPrompt(PropertyInfo prop, Mobile mobile, object o, Stack stack, Type type, int page, ArrayList list) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Type = type; + m_Page = page; + m_List = list; + } + + public override void OnCancel(Mobile from) + { + m_Mobile.SendGump(new XmlSetObjectGump(m_Property, m_Mobile, m_Object, m_Stack, m_Type, m_Page, m_List)); + } + + public override void OnResponse(Mobile from, string text) + { + object toSet; + bool shouldSet; + + try + { + var serial = Utility.ToUInt32(text); + toSet = World.FindEntity((Serial)serial); + + if (toSet == null) + { + shouldSet = false; + m_Mobile.SendMessage("No object with that serial was found."); + } + else if (!m_Type.IsInstanceOfType(toSet)) + { + toSet = null; + shouldSet = false; + m_Mobile.SendMessage($"The object with that serial could not be assigned to a property of type : {m_Type.Name}"); + } + else + { + shouldSet = true; + } + } + catch + { + toSet = null; + shouldSet = false; + m_Mobile.SendMessage("Bad format"); + } + + if (shouldSet) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet.ToString()); + m_Property.SetValue(m_Object, toSet, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + m_Mobile.SendGump(new XmlSetObjectGump(m_Property, m_Mobile, m_Object, m_Stack, m_Type, m_Page, m_List)); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + bool shouldSet, shouldSend = true; + object viewProps = null; + + switch (info.ButtonID) + { + case 0: // closed + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + shouldSet = false; + shouldSend = false; + + break; + } + case 1: // Change by Target + { + m_Mobile.Target = new XmlSetObjectTarget(m_Property, m_Mobile, m_Object, m_Stack, m_Type, m_Page, m_List); + shouldSet = false; + shouldSend = false; + break; + } + case 2: // Change by Serial + { + shouldSet = false; + shouldSend = false; + m_Mobile.SendMessage("Enter the serial you wish to find:"); + m_Mobile.Prompt = new InternalPrompt(m_Property, m_Mobile, m_Object, m_Stack, m_Type, m_Page, m_List); + + break; + } + case 3: // Nullify + { + shouldSet = true; + break; + } + case 4: // View Properties + { + shouldSet = false; + + var obj = m_Property.GetValue(m_Object, null); + + if (obj == null) + { + m_Mobile.SendMessage("The property is null and so you cannot view its properties."); + } + else if (!BaseCommand.IsAccessible(m_Mobile, obj)) + { + m_Mobile.SendMessage("You may not view their properties."); + } + else + { + viewProps = obj; + } + + break; + } + default: + { + shouldSet = false; + break; + } + } + + if (shouldSet) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, "(null)"); + m_Property.SetValue(m_Object, null, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + if (shouldSend) + { + m_Mobile.SendGump(new XmlSetObjectGump(m_Property, m_Mobile, m_Object, m_Stack, m_Type, m_Page, m_List)); + } + + if (viewProps != null) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, viewProps)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetObjectTarget.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetObjectTarget.cs new file mode 100644 index 000000000..303c03d69 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetObjectTarget.cs @@ -0,0 +1,72 @@ +using Server.Commands; +using Server.Items; +using Server.Targeting; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetObjectTarget : Target +{ + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly Type m_Type; + private readonly int m_Page; + private readonly ArrayList m_List; + + public XmlSetObjectTarget(PropertyInfo prop, Mobile mobile, object o, Stack stack, Type type, int page, ArrayList list) : base(-1, false, TargetFlags.None) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Type = type; + m_Page = page; + m_List = list; + } + + protected override void OnTarget(Mobile from, object targeted) + { + try + { + if (m_Type == typeof(Type)) + { + targeted = targeted.GetType(); + } + else if ((m_Type == typeof(BaseAddon) || m_Type.IsAssignableFrom(typeof(BaseAddon))) && targeted is AddonComponent component) + { + targeted = component.Addon; + } + + if (m_Type.IsInstanceOfType(targeted)) + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, targeted.ToString()); + m_Property.SetValue(m_Object, targeted, null); + } + else + { + m_Mobile.SendMessage($"That cannot be assigned to a property of type : {m_Type.Name}"); + } + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + protected override void OnTargetFinish(Mobile from) + { + if (m_Type == typeof(Type)) + { + from.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + else + { + from.SendGump(new XmlSetObjectGump(m_Property, m_Mobile, m_Object, m_Stack, m_Type, m_Page, m_List)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetPoint2DGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetPoint2DGump.cs new file mode 100644 index 000000000..ed9f30b83 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetPoint2DGump.cs @@ -0,0 +1,235 @@ +using Server.Commands; +using Server.Network; +using Server.Targeting; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetPoint2DGump : Gump +{ + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int CoordWidth = 105; + private static readonly int EntryWidth = CoordWidth + OffsetSize + CoordWidth; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + private static readonly int TotalHeight = OffsetSize + 4 * (EntryHeight + OffsetSize); + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + private static readonly int BackHeight = BorderSize + TotalHeight + BorderSize; + + public XmlSetPoint2DGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(GumpOffsetX, GumpOffsetY) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + + var p = (Point2D)prop.GetValue(o, null); + + AddPage(0); + + AddBackground(0, 0, BackWidth, BackHeight, BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), TotalHeight, OffsetGumpID); + + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, prop.Name); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "Use your location"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 1); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "Target a location"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 2); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "X:"); + AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 0, p.X.ToString()); + x += CoordWidth + OffsetSize; + + AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "Y:"); + AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 1, p.Y.ToString()); + x += CoordWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 3); + } + + private class InternalTarget : Target + { + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public InternalTarget(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(-1, true, TargetFlags.None) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + } + + protected override void OnTarget(Mobile from, object targeted) + { + var p = targeted as IPoint3D; + + if (p != null) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, new Point2D(p.X, p.Y).ToString()); + m_Property.SetValue(m_Object, new Point2D(p.X, p.Y), null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + } + + protected override void OnTargetFinish(Mobile from) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + Point2D toSet; + bool shouldSet, shouldSend; + + switch (info.ButtonID) + { + case 1: // Current location + { + toSet = new Point2D(m_Mobile.X, m_Mobile.Y); + shouldSet = true; + shouldSend = true; + + break; + } + case 2: // Pick location + { + m_Mobile.Target = new InternalTarget(m_Property, m_Mobile, m_Object, m_Stack, m_Page, m_List); + + toSet = Point2D.Zero; + shouldSet = false; + shouldSend = false; + + break; + } + case 3: // Use values + { + var x = info.GetTextEntry(0); + var y = info.GetTextEntry(1); + + toSet = new Point2D(x == null ? 0 : Utility.ToInt32(x.Text), y == null ? 0 : Utility.ToInt32(y.Text)); + shouldSet = true; + shouldSend = true; + + break; + } + default: + { + toSet = Point2D.Zero; + shouldSet = false; + shouldSend = true; + + break; + } + } + + if (shouldSet) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet.ToString()); + m_Property.SetValue(m_Object, toSet, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + if (shouldSend) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetPoint3DGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetPoint3DGump.cs new file mode 100644 index 000000000..e6d4a99a9 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetPoint3DGump.cs @@ -0,0 +1,241 @@ +using Server.Commands; +using Server.Network; +using Server.Targeting; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetPoint3DGump : Gump +{ + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int CoordWidth = 70; + private static readonly int EntryWidth = CoordWidth + OffsetSize + CoordWidth + OffsetSize + CoordWidth; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + private static readonly int TotalHeight = OffsetSize + 4 * (EntryHeight + OffsetSize); + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + private static readonly int BackHeight = BorderSize + TotalHeight + BorderSize; + + public XmlSetPoint3DGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(GumpOffsetX, GumpOffsetY) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + + var p = (Point3D)prop.GetValue(o, null); + + AddPage(0); + + AddBackground(0, 0, BackWidth, BackHeight, BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), TotalHeight, OffsetGumpID); + + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, prop.Name); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "Use your location"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 1); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, "Target a location"); + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 2); + + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "X:"); + AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 0, p.X.ToString()); + x += CoordWidth + OffsetSize; + + AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "Y:"); + AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 1, p.Y.ToString()); + x += CoordWidth + OffsetSize; + + AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "Z:"); + AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 2, p.Z.ToString()); + x += CoordWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 3); + } + + private class InternalTarget : Target + { + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public InternalTarget(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(-1, true, TargetFlags.None) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + } + + protected override void OnTarget(Mobile from, object targeted) + { + var p = targeted as IPoint3D; + + if (p != null) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, new Point3D(p).ToString()); + m_Property.SetValue(m_Object, new Point3D(p), null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + } + + protected override void OnTargetFinish(Mobile from) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + Point3D toSet; + bool shouldSet, shouldSend; + + switch (info.ButtonID) + { + case 1: // Current location + { + toSet = m_Mobile.Location; + shouldSet = true; + shouldSend = true; + + break; + } + case 2: // Pick location + { + m_Mobile.Target = new InternalTarget(m_Property, m_Mobile, m_Object, m_Stack, m_Page, m_List); + + toSet = Point3D.Zero; + shouldSet = false; + shouldSend = false; + + break; + } + case 3: // Use values + { + var x = info.GetTextEntry(0); + var y = info.GetTextEntry(1); + var z = info.GetTextEntry(2); + + toSet = new Point3D(x == null ? 0 : Utility.ToInt32(x.Text), y == null ? 0 : Utility.ToInt32(y.Text), z == null ? 0 : Utility.ToInt32(z.Text)); + shouldSet = true; + shouldSend = true; + + break; + } + default: + { + toSet = Point3D.Zero; + shouldSet = false; + shouldSend = true; + + break; + } + } + + if (shouldSet) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet.ToString()); + m_Property.SetValue(m_Object, toSet, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + if (shouldSend) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetTimeSpanGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetTimeSpanGump.cs new file mode 100644 index 000000000..79550de54 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlPropsGumps/XmlSetTimeSpanGump.cs @@ -0,0 +1,240 @@ +using Server.Commands; +using Server.Network; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlSetTimeSpanGump : Gump +{ + private readonly PropertyInfo m_Property; + private readonly Mobile m_Mobile; + private readonly object m_Object; + private readonly Stack m_Stack; + private readonly int m_Page; + private readonly ArrayList m_List; + + public static readonly bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = PropsConfig.EntryHeight; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly int EntryWidth = 212; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + private static readonly int TotalHeight = OffsetSize + 7 * (EntryHeight + OffsetSize); + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + private static readonly int BackHeight = BorderSize + TotalHeight + BorderSize; + + public XmlSetTimeSpanGump(PropertyInfo prop, Mobile mobile, object o, Stack stack, int page, ArrayList list) : base(GumpOffsetX, GumpOffsetY) + { + m_Property = prop; + m_Mobile = mobile; + m_Object = o; + m_Stack = stack; + m_Page = page; + m_List = list; + + var ts = (TimeSpan)prop.GetValue(o, null); + + AddPage(0); + + AddBackground(0, 0, BackWidth, BackHeight, BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), TotalHeight, OffsetGumpID); + + AddRect(0, prop.Name, 0, -1); + AddRect(1, ts.ToString(), 0, -1); + AddRect(2, "Zero", 1, -1); + AddRect(3, "From H:M:S", 2, -1); + AddRect(4, "H:", 3, 0); + AddRect(5, "M:", 4, 1); + AddRect(6, "S:", 5, 2); + } + + private void AddRect(int index, string str, int button, int text) + { + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize + index * (EntryHeight + OffsetSize); + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y, EntryWidth - TextOffsetX, EntryHeight, TextHue, str); + + if (text != -1) + { + AddTextEntry(x + 16 + TextOffsetX, y, EntryWidth - TextOffsetX - 16, EntryHeight, TextHue, text, ""); + } + + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + if (button != 0) + { + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, button); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + TimeSpan toSet; + bool shouldSet, shouldSend; + + var h = info.GetTextEntry(0); + var m = info.GetTextEntry(1); + var s = info.GetTextEntry(2); + + switch (info.ButtonID) + { + case 1: // Zero + { + toSet = TimeSpan.Zero; + shouldSet = true; + shouldSend = true; + + break; + } + case 2: // From H:M:S + { + if (h != null && m != null && s != null) + { + try + { + toSet = TimeSpan.Parse($"{h.Text}:{m.Text}:{s.Text}"); + shouldSet = true; + shouldSend = true; + + break; + } + catch + { + } + } + + toSet = TimeSpan.Zero; + shouldSet = false; + shouldSend = false; + + break; + } + case 3: // From H + { + if (h != null) + { + try + { + toSet = TimeSpan.FromHours(Utility.ToDouble(h.Text)); + shouldSet = true; + shouldSend = true; + + break; + } + catch + { + } + } + + toSet = TimeSpan.Zero; + shouldSet = false; + shouldSend = false; + + break; + } + case 4: // From M + { + if (m != null) + { + try + { + toSet = TimeSpan.FromMinutes(Utility.ToDouble(m.Text)); + shouldSet = true; + shouldSend = true; + + break; + } + catch + { + } + } + + toSet = TimeSpan.Zero; + shouldSet = false; + shouldSend = false; + + break; + } + case 5: // From S + { + if (s != null) + { + try + { + toSet = TimeSpan.FromSeconds(Utility.ToDouble(s.Text)); + shouldSet = true; + shouldSend = true; + + break; + } + catch + { + } + } + + toSet = TimeSpan.Zero; + shouldSet = false; + shouldSend = false; + + break; + } + default: + { + toSet = TimeSpan.Zero; + shouldSet = false; + shouldSend = true; + + break; + } + } + + if (shouldSet) + { + try + { + CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet.ToString()); + m_Property.SetValue(m_Object, toSet, null); + } + catch + { + m_Mobile.SendMessage("An exception was caught. The property may not have changed."); + } + } + + if (shouldSend) + { + m_Mobile.SendGump(new XmlPropertiesGump(m_Mobile, m_Object, m_Stack, m_List, m_Page)); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlSpawner.cs b/Projects/UOContent/Engines/XMLSpawner/XmlSpawner.cs new file mode 100644 index 000000000..b7bcd12c2 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlSpawner.cs @@ -0,0 +1,11948 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Xml; + +using Server.Accounting; +using Server.Commands; +using Server.Commands.Generic; +using Server.ContextMenus; +using Server.Engines.Spawners; +using Server.Items; +using Server.Network; +using Server.Targeting; + +namespace Server.Mobiles; + +public class XmlSpawner : Item, ISpawner +{ + public enum TODModeType { Realtime, Gametime } + + public enum SpawnPositionType { Random, RowFill, ColFill, Perimeter, Player, Waypoint, RelXY, DeltaLocation, Location, Wet, Tiles, NoTiles, ItemID, NoItemID } + + public class SpawnPositionInfo + { + public SpawnPositionType positionType; + public Mobile trigMob; + public string[] positionArgs; + + public SpawnPositionInfo(SpawnPositionType positiontype, Mobile trigmob, string[] positionargs) + { + positionType = positiontype; + trigMob = trigmob; + positionArgs = positionargs; + } + } + + public class MovementInfo + { + public Mobile trigMob; + public Point3D trigLocation = Point3D.Zero; + + public MovementInfo(Mobile m) + { + trigMob = m; + if (m != null) + { + trigLocation = m.Location; + } + } + } + + public const string Version = "5.0"; // RUADUCK's EDIT + + public const byte MaxLoops = 10; //maximum number of recursive calls from spawner to itself. this is to prevent stack overflow from xmlspawner scripting + + private const int ShowBoundsItemId = 14089; // 14089 Fire Column // 3555 Campfire // 8708 Skull Pole + + private const string SpawnDataSetName = "Spawns"; + private const string SpawnTablePointName = "Points"; + + private const int SpawnFitSize = 16; // Normal wall/door height for a mobile is 20 to walk through + + private static int BaseItemId = 0x1F1C; // Purple Magic Crystal + private static int ShowItemId = 0x3E57; // ships mast + + private static int defaultTriggerSound = 0x1F4; // click and sparkle sound by default (0x1F4) , click sound (0x3A4) + + public static string XmlSpawnDir { get; set; } = "XmlSpawner"; // default directory for saving/loading .xml files with [xmlload [xmlsave + + private const int MaxSmartSectorListSize = 1024; // maximum sector list size for use in smart spawning. This gives a 512x512 tile range. + + private static string defwaypointname; // default waypoint name will get assigned in Initialize + + private const string XmlTableName = "Properties"; + private const string XmlDataSetName = "XmlSpawner"; + + public static AccessLevel DiskAccessLevel { get; set; } = AccessLevel.Administrator; // minimum access level required by commands that can access the disk such as XmlLoad, XmlSave, and the Save function of XmlEdit + +#if RESTRICTConstructible + public static AccessLevel ConstructibleAccessLevel { get; set; } = AccessLevel.GameMaster; // only allow spawning of objects that have Constructible access restrictions at this level or lower. Must define RESTRICTConstructible to enable this. +#endif + + private static int MaxMoveCheck = 10; // limit number of players that can be checked for triggering in a single OnMovement tick + + // specifies the level at which smartspawning will be triggered. Players with AccessLevel above this will not trigger smartspawning unless unhidden. + public static AccessLevel SmartSpawnAccessLevel { get; set; } = AccessLevel.Player; + + // define the default values used in making spawners + private static TimeSpan defMinDelay = TimeSpan.FromMinutes(5); + private static TimeSpan defMaxDelay = TimeSpan.FromMinutes(10); + private static TimeSpan defMinRefractory = TimeSpan.FromMinutes(0); + private static TimeSpan defMaxRefractory = TimeSpan.FromMinutes(0); + private static TimeSpan defTODStart = TimeSpan.FromMinutes(0); + private static TimeSpan defTODEnd = TimeSpan.FromMinutes(0); + private static TimeSpan defDuration = TimeSpan.FromMinutes(0); + private static readonly TimeSpan defDespawnTime = TimeSpan.FromHours(0); + + private static bool defIsGroup; + private static int defTeam; + private static int defProximityTriggerSound = defaultTriggerSound; + private static int defAmount = 1; + private static bool defRelativeHome = true; + private static int defSpawnRange = 5; + private static int defHomeRange = 5; + private static double defTriggerProbability = 1; + private static int defProximityRange = -1; + private static readonly int defKillReset = 1; + private static TODModeType defTODMode = TODModeType.Realtime; + + private static Timer m_GlobalSectorTimer; + private static bool SmartSpawningSystemEnabled; + + private static WarnTimer2 m_WarnTimer; + + // hash table for optimizing HoldSmartSpawning method invocation + private static Dictionary holdSmartSpawningHash; + + public static int seccount { get; set; } + + // sector hashtable for each map + private static readonly Dictionary>[] GlobalSectorTable = new Dictionary>[6]; + + private string m_Name = string.Empty; + private int m_Team; + private int m_HomeRange; + + // this is actually redundant with the width height spec for spawning area + // just an easier way of specifying it + private int m_SpawnRange; + private int m_Count; + private TimeSpan m_MinDelay; + private TimeSpan m_MaxDelay; + // added a duration parameter for time-limited spawns + private TimeSpan m_Duration; + private List m_SpawnObjects = new(); // List of objects to spawn + private DateTime m_End; + private DateTime m_RefractEnd; + private DateTime m_DurEnd; + private SpawnerTimer m_Timer; + private InternalTimer m_DurTimer; + private InternalTimer3 m_RefractoryTimer; + private bool m_Running; + private bool m_Group; + private int m_X; + private int m_Y; + private int m_Width; + private int m_Height; + private Static m_ShowContainerStatic; + private bool m_proximityActivated; + private bool m_refractActivated; + private bool m_durActivated; + private string m_ItemTriggerName; + private string m_NoItemTriggerName; + private Item m_ObjectPropertyItem; + private string m_ObjectPropertyName; + + public string status_str { get; set; } + + private int m_killcount; + // added proximity range sensor + private int m_ProximityRange; + private bool m_speechTriggerActivated; + private bool m_skipped; + private int m_spawncheck; + private DateTime m_SeqEnd; + private Region m_Region; // 2004.02.08 :: Omega Red + private string m_RegionName = string.Empty; // 2004.02.08 :: Omega Red + + public List m_TextEntryBook; + private bool m_OnHold; + private bool m_HoldSequence; + private List m_MovementList; + private MovementTimer m_MovementTimer; + + private List m_KeywordTagList = new(); + + public List RecentSpawnerSearchList { get; set; } + public List RecentItemSearchList { get; set; } + public List RecentMobileSearchList { get; set; } + + private SkillName m_skill_that_triggered; + + private Map currentmap; + + private bool m_IsInactivated; + private bool m_SmartSpawning; + private SectorTimer m_SectorTimer; + + private List m_ShowBoundsItems = new(); + + public List PropertyInfoList { get; set; } // used to optimize property info lookup used by set and get property methods. + + private Dictionary> spawnPositionWayTable; // used to optimize #waypoint lookup + + private bool inrespawn; + + private List sectorList; + private Point3D mostRecentSpawnPosition = Point3D.Zero; + + // does not decay + public override bool Decays => false; + // is not counted in the normal item count + public override bool IsVirtualItem => true; + + // private bool m_skillTriggerActivated; + // private SkillName m_SkillTriggerName; + // private double m_SkillTriggerMin; + // private double m_SkillTriggerMax; + // private int m_SkillTriggerSuccess; + + public bool DebugThis { get; set; } + + public int MovingPlayerCount { get; set; } + + public int FastestPlayerSpeed { get; set; } + + public int NearbyPlayerCount + { + get + { + var count = 0; + if (ProximityRange >= 0) + { + foreach (var m in GetMobilesInRange(ProximityRange)) + { + if (m?.Player == true) + { + count++; + } + } + } + return count; + } + } + + public Point3D MostRecentSpawnPosition + { + get => mostRecentSpawnPosition; + set => mostRecentSpawnPosition = value; + } + + public TimeSpan GameTOD + { + get + { + + Clock.GetTime(Map, Location.X, Location.Y, out var hours, out int minutes); + return new DateTime(Core.Now.Year, Core.Now.Month, Core.Now.Day, hours, minutes, 0).TimeOfDay; + } + } + + public static TimeSpan RealTOD => Core.Now.TimeOfDay; + + public static int RealDay => Core.Now.Day; + + public static int RealMonth => Core.Now.Month; + + public static DayOfWeek RealDayOfWeek => Core.Now.DayOfWeek; + + public MoonPhase MoonPhase => Clock.GetMoonPhase(Map, Location.X, Location.Y); + + public XmlSpawnerGump SpawnerGump { get; set; } + + public bool DisableGlobalAutoReset { get; set; } + + public bool DoDefrag + { + get => false; + set + { + if (value) + { + Defrag(true); + } + } + } + + private const bool SectorIsActive = false; + + public bool SingleSector { get; private set; } + + public static bool InActivationRange(Sector s1, Sector s2) + { + // check to see if the sectors are within +- 2 of one another + if (s1 == null || s2 == null) + { + return false; + } + + return Math.Abs(s1.X - s2.X) < 3 && Math.Abs(s1.Y - s2.Y) < 3; + } + + public bool HasDamagedOrDistantSpawns + { + get + { + var ssec = Map.GetSector(Location); + // go through the spawn lists + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + + if (o is not BaseCreature creature) + { + continue; + } + + // if the mob is damaged or outside of smartspawning detection range then return true + if (creature.Hits < creature.HitsMax || creature.Mana < creature.ManaMax || creature.Stam < creature.StamMax || creature.Map != Map) + { + return true; + } + + // if the spawn moves into a sector that is not activatable from a sector on the sector list then dont smartspawn + if (creature.Map != null && creature.Map != Map.Internal) + { + var bsec = creature.Map.GetSector(creature.Location); + + if (SingleSector) + { + // is it in activatable range of the sector the spawner is in + if (!InActivationRange(bsec, ssec)) + { + return true; + } + } + else + { + var outofsec = true; + + if (sectorList != null) + { + foreach (var s in sectorList) + { + // is the creatures sector within activation range of any of the sectors in the list + if (InActivationRange(bsec, s)) + { + outofsec = false; + break; + } + } + } + + if (outofsec) + { + return true; + } + } + } + } + } + + return false; + } + } + + private static int totalSectorsMonitored; + + public bool HasActiveSectors + { + get + { + if (!SmartSpawning || Map == null || Map == Map.Internal) + { + return false; + } + + // is this a region spawner? + if (m_Region != null) + { + var players = m_Region.GetPlayers(); + + if (players == null || players.Count == 0) + { + return false; + } + + // confirm that players with the proper access level are present + foreach (var m in players) + { + if (m != null && (m.AccessLevel <= SmartSpawnAccessLevel || !m.Hidden)) + { + return true; + } + } + return false; + } + // is this a single sector spawner? + if (SingleSector) + { + return SectorIsActive; + } + + // if there is no sector list made for this spawner then create one. + if (sectorList == null) + { + var loc = Location; + sectorList = new List(); + + // is this container held? + if (Parent != null) + { + if (RootParent is IPoint3D e) + { + loc = new Point3D(e); + } + } + + // find the max detection range by examining both spawnrange + // note, sectors will activate when within +-2 sectors + var bufferzone = 2 * Map.SectorSize; + var x1 = m_X - bufferzone; + var width = m_Width + 2 * bufferzone; + var y1 = m_Y - bufferzone; + var height = m_Height + 2 * bufferzone; + + // go through all of the sectors within the SpawnRange of the spawner to see if any are active + for (var x = x1; x <= x1 + width; x += Map.SectorSize) + { + for (var y = y1; y <= y1 + height; y += Map.SectorSize) + { + var s = Map.GetSector(new Point3D(x, y, loc.Z)); + + if (s == null) + { + continue; + } + + // dont add any redundant sectors + var duplicate = false; + foreach (var olds in sectorList) + { + if (olds == s) + { + duplicate = true; + break; + } + } + if (!duplicate) + { + sectorList.Add(s); + + if (GlobalSectorTable[Map.MapID] == null) + { + GlobalSectorTable[Map.MapID] = new Dictionary>(); + } + + // add this sector and the spawner associated with it to the global sector table + if (GlobalSectorTable[Map.MapID].TryGetValue(s, out var spawnerlist)) //.Contains(s)) + { + //List spawnerlist = GlobalSectorTable[Map.MapID][s]; + if (spawnerlist == null) + { + //GlobalSectorTable[Map.MapID].Remove(s); + spawnerlist = new List(); + //GlobalSectorTable[Map.MapID].Add(s, spawnerlist); + GlobalSectorTable[Map.MapID][s] = spawnerlist; + } + + if (!spawnerlist.Contains(this)) + { + spawnerlist.Add(this); + + } + } + else + { + spawnerlist = new List + { + this + }; + // add a new entry to the table + GlobalSectorTable[Map.MapID][s] = spawnerlist; + } + + totalSectorsMonitored++; + + // add some sanity checking here + if (sectorList.Count > MaxSmartSectorListSize) + { + SmartSpawning = false; + + // log it + try + { + Console.WriteLine("SmartSpawning disabled at {0} {1} : Range too large.", loc, Map); + + using var op = new StreamWriter("badspawn.log", true); + op.WriteLine("{0} SmartSpawning disabled at {1} {2} : Range too large.", Core.Now, loc, Map); + op.WriteLine(); + } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + + return true; + } + } + } + } + + SingleSector = false; + } + + TraceStart(2); + // go through the sectorlist and see if any of the sectors are active + + foreach (var s in sectorList) + { + if (s != null && s.Active && s.Clients != null && s.Clients.Count > 0) + { + // confirm that players with the proper access level are present + foreach (var ns in s.Clients) + { + var m = ns.Mobile; + if (m != null && (m.AccessLevel <= SmartSpawnAccessLevel || !m.Hidden)) + { + return true; + } + } + TraceEnd(2); + } + seccount++; + } + TraceEnd(2); + return false; + } + } + + public static int SecCount => seccount; + + public bool IsInactivated + { + get => m_IsInactivated; + set => m_IsInactivated = value; + } + + public int ActiveSectorCount + { + get + { + if (sectorList != null) + { + return sectorList.Count; + } + + return 0; + } + } + + public bool PlayerCreated { get; set; } + + public bool OnHold + { + get + { + if (m_OnHold) + { + return true; + } + + // determine whether there are any keywordtags with the hold flag + if (m_KeywordTagList == null || m_KeywordTagList.Count == 0) + { + return false; + } + + foreach (var sot in m_KeywordTagList) + { + // check for any keyword tag with the holdspawn flag + if (sot?.Deleted == false && (sot.Flags & BaseXmlSpawner.KeywordFlags.HoldSpawn) != 0) + { + return true; + } + } + // no hold flags were set + return false; + } + set => m_OnHold = value; + } + + public string AddSpawn + { + get => null; + set + { + if (!string.IsNullOrEmpty(value)) + { + var str = value.Trim(); + var typestr = BaseXmlSpawner.ParseObjectType(str); + + var type = AssemblyHandler.FindTypeByName(typestr); + + if (type != null) + { + m_SpawnObjects.Add(new SpawnObject(str, 1)); + } + else + { + // check for special keywords + if (typestr != null && (BaseXmlSpawner.IsTypeOrItemKeyword(typestr) || typestr.IndexOf("{") != -1 || typestr.StartsWith("*") || typestr.StartsWith("#"))) + { + m_SpawnObjects.Add(new SpawnObject(str, 1)); + } + else + { + status_str = $"{str} is not a valid type name."; + } + } + InvalidateProperties(); + } + } + } + + public string UniqueId { get; private set; } = string.Empty; + + // does not perform a defrag, so less accurate but can be used while looping through world object enums + public int SafeCurrentCount => SafeTotalSpawnedObjects; + + public bool FreeRun { get; set; } + + public bool CanFreeSpawn + { + get + { + // allow free spawning if proximity sensing is off and if all of the potential free-spawning triggers are disabled + if (Running && m_ProximityRange == -1 && + string.IsNullOrEmpty(m_ObjectPropertyName) && + (string.IsNullOrEmpty(MobTriggerProp) || + MobTriggerName == null || MobTriggerName.Length == 0) && + !ExternalTriggering) + { + return true; + } + + return false; + } + } + + public SpawnObject[] SpawnObjects + { + get => m_SpawnObjects.ToArray(); + set + { + if (value?.Length > 0) + { + + foreach (var so in value) + { + if (so == null) + { + continue; + } + + var AlreadyInList = false; + + // Check if the new array has an existing spawn object + foreach (var TheSpawn in m_SpawnObjects) + { + if (TheSpawn.TypeName.ToUpper() == so.TypeName.ToUpper()) + { + AlreadyInList = true; + break; + } + } + + // Does this item need to be added + if (!AlreadyInList) + { + // This is a new spawn object so add it to the array (deep copy) + m_SpawnObjects.Add(new SpawnObject(so.TypeName, so.ActualMaxCount, so.SubGroup, so.SequentialResetTime, so.SequentialResetTo, so.KillsNeeded, + so.RestrictKillsToSubgroup, so.ClearOnAdvance, so.MinDelay, so.MaxDelay, so.SpawnsPerTick, so.PackRange)); + } + } + + if (SpawnObjects.Length < 1) + { + Stop(); + } + + InvalidateProperties(); + } + } + } + + public bool HoldSequence + { + get + { + // check to see if any keyword tags have the holdsequence flag set, or whether the spawner holdsequence flag is set + if (m_HoldSequence) + { + return true; + } + + // determine whether there are any keywordtags with the hold flag + if (m_KeywordTagList == null || m_KeywordTagList.Count == 0) + { + return false; + } + + foreach (var sot in m_KeywordTagList) + { + // check for any keyword tag with the holdsequence flag + if (sot?.Deleted == false && (sot.Flags & BaseXmlSpawner.KeywordFlags.HoldSequence) != 0) + { + return true; + } + } + + // no hold flags were set + return false; + } + + set => m_HoldSequence = value; + } + + public bool CanSpawn + { + get + { + if (OnHold) + { + return false; + } + + if (m_Group) + { + if (TotalSpawnedObjects <= 0) + { + return true; + } + + return false; + } + + if (IsFull) + { + return false; + } + + return true; + } + } + + // test for a full spawner + public bool IsFull + { + get + { + var nobj = TotalSpawnedObjects; + + return nobj >= m_Count || nobj >= TotalSpawnObjectCount; + } + } + + // this can be used in loops over world objects since it will not defrag and potentially modify the world object lists + public int SafeTotalSpawnedObjects + { + get + { + if (m_SpawnObjects == null) + { + return 0; + } + + var count = 0; + + foreach (var so in m_SpawnObjects) + { + count += so.SpawnedObjects.Count; + } + + return count; + } + } + + public int TotalSpawnedObjects + { + get + { + if (m_SpawnObjects == null) + { + return 0; + } + + // defrag so that accurately reflects currently active spawns + Defrag(true); + + var count = 0; + + foreach (var so in m_SpawnObjects) + { + count += so.SpawnedObjects.Count; + } + + return count; + } + } + + public int TotalSpawnObjectCount + { + get + { + var count = 0; + + foreach (var so in m_SpawnObjects) + { + count += so.MaxCount; + } + + return count; + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool GumpReset + { + + set + { + if (value) + { + SpawnerGump = null; + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public Region SpawnRegion + { + get => m_Region; + set + { + // force a re-update of the smart spawning sector list the next time it is accessed + ResetSectorList(); + + m_Region = value; + + m_RegionName = m_Region?.Name; + } + } + + // 2004.02.08 :: Omega Red + [CommandProperty(AccessLevel.GameMaster)] + public string RegionName + { + get => m_RegionName; + set + { + // force a re-update of the smart spawning sector list the next time it is accessed + ResetSectorList(); + + m_RegionName = value; + + if (string.IsNullOrEmpty(value)) + { + m_Region = null; + return; + } + + if (Region.Regions.Count == 0) // after world load, before region load + { + return; + } + + foreach (var region in Region.Regions) + { + if (region.Name.InsensitiveEquals(m_RegionName)) + { + m_Region = region; + m_RegionName = region.Name; + //InvalidateProperties(); + return; + } + } + status_str = $"invalid region: {value}"; + m_Region = null; + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public Point3D X1_Y1 + { + get => new(m_X, m_Y, Z); + set + { + // X1 and Y1 will initiate region specification + m_Width = 0; + m_Height = 0; + m_X = value.X; + m_Y = value.Y; + + // reset the sector list + ResetSectorList(); + + m_SpawnRange = 0; + + if (ShowBounds) + { + ShowBounds = false; + ShowBounds = true; + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public Point3D X2_Y2 + { + get => new(m_X + m_Width, m_Y + m_Height, Z); + set + { + int X2; + int Y2; + + var OriginalX2 = m_X + m_Width; + var OriginalY2 = m_Y + m_Height; + + // reset the sector list + ResetSectorList(); + + // now determine based upon the entered coordinate values what the lower left corner is + // lower left will be the min x and min y + // upper right will be max x max y + if (value.X < OriginalX2) + { + // ok, this is the proper x value for the lower left + m_X = value.X; + X2 = OriginalX2; + } + else + { + m_X = OriginalX2; + X2 = value.X; + } + + if (value.Y < OriginalY2) + { + // ok, this is the proper y value for the lower left + m_Y = value.Y; + Y2 = OriginalY2; + } + else + { + m_Y = OriginalY2; + Y2 = value.Y; + } + + m_Width = X2 - m_X; + m_Height = Y2 - m_Y; + + if (m_Width == m_Height) + { + m_SpawnRange = m_Width / 2; + } + else + { + m_SpawnRange = -1; + } + + if (HomeRangeIsRelative == false) + { + var NewHomeRange = m_Width > m_Height ? m_Height : m_Width; + m_HomeRange = NewHomeRange > 0 ? NewHomeRange : 0; + } + + //original test was for less than 1, changed it to less than zero (zero is a valid width, its the default in fact) + // Stop the spawner if the width or height is less than 1 + if (m_Width < 0 || m_Height < 0) + { + Running = false; + } + + InvalidateProperties(); + + if (ShowBounds) + { + ShowBounds = false; + ShowBounds = true; + } + } + } + + // added the spawnrange property. It sets both the XY and width/height parameters automatically. + // also doesnt mess with homerange like XY does + [CommandProperty(AccessLevel.GameMaster)] + public int SpawnRange + { + get => m_SpawnRange; + set + { + if (value < 0) + { + return; + } + + // reset the sector list + ResetSectorList(); + + m_SpawnRange = value; + m_Width = m_SpawnRange * 2; + m_Height = m_SpawnRange * 2; + + // dont set the bounding box locations if the initial location is 0,0 since this occurs when the item is just being made + // because m_X and m_Y are restored on loading, it creates problems with OnLocationChange which has to avoid applying translational + // adjustments to newly placed spawners (because the actual m_X and m_Y is associated with the original location, not the 0,0 location) + // basically, before placement, dont set m_X or m_Y to anything that needs to be adjusted later on + + if (Location.X == 0 && Location.Y == 0) + { + return; + } + + m_X = Location.X - m_SpawnRange; + m_Y = Location.Y - m_SpawnRange; + + if (ShowBounds) + { + ShowBounds = false; + ShowBounds = true; + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool ShowBounds + { + get => m_ShowBoundsItems?.Count > 0; + set + { + if (value && ShowBounds == false) + { + m_ShowBoundsItems ??= new List(); + + // Boundary lines + var ValidX1 = m_X; + var ValidX2 = m_X + m_Width; + var ValidY1 = m_Y; + var ValidY2 = m_Y + m_Height; + + for (var x = 0; x <= m_Width; x++) + { + var NewX = m_X + x; + for (var y = 0; y <= m_Height; y++) + { + var NewY = m_Y + y; + + if (NewX == ValidX1 || NewX == ValidX2 || NewX == ValidY1 || NewX == ValidY2 || NewY == ValidX1 || NewY == ValidX2 || NewY == ValidY1 || NewY == ValidY2) + { + // Add an object to show the spawn area + var s = new Static(ShowBoundsItemId) + { + Visible = false + }; + s.MoveToWorld(new Point3D(NewX, NewY, Z), Map); + m_ShowBoundsItems.Add(s); + } + } + } + } + + if (value == false && m_ShowBoundsItems != null) + { + // Remove all of the items from the array + foreach (var s in m_ShowBoundsItems) + { + s.Delete(); + } + + m_ShowBoundsItems.Clear(); + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public int MaxCount + { + get => m_Count; + set + { + m_Count = value; + InvalidateProperties(); + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public int CurrentCount => TotalSpawnedObjects; + + [CommandProperty(AccessLevel.GameMaster)] + public WayPoint WayPoint { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool ExternalTriggering { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool ExtTrigState { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool Running + { + get => m_Running; + set + { + // Don't start the spawner unless the height and width are valid + if (value && m_Width >= 0 && m_Height >= 0) + { + Start(); + } + else + { + Stop(); + } + + InvalidateProperties(); + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public int HomeRange + { + get => m_HomeRange; + set { m_HomeRange = value; InvalidateProperties(); } + } + + public Region Region { get; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool HomeRangeIsRelative { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public int Team + { + get => m_Team; + set { m_Team = value; InvalidateProperties(); } + } + [CommandProperty(AccessLevel.GameMaster)] + public int StackAmount { get; set; } + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan MinDelay + { + get => m_MinDelay; + set + { + m_MinDelay = value; + // reset the spawn timer + DoTimer(); + InvalidateProperties(); + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan MaxDelay + { + get => m_MaxDelay; + set + { + m_MaxDelay = value; + // reset the spawn timer + DoTimer(); + InvalidateProperties(); + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public int KillCount + { + get => m_killcount; + set => m_killcount = value; + } + [CommandProperty(AccessLevel.GameMaster)] + public int KillReset { get; set; } = defKillReset; + + [CommandProperty(AccessLevel.GameMaster)] + public double TriggerProbability { get; set; } = defTriggerProbability; + + //added refractory period support + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan RefractMin { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan RefractMax { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan RefractoryOver + { + get + { + if (m_refractActivated) + { + return m_RefractEnd - Core.Now; + } + + return TimeSpan.FromSeconds(0); + } + set => DoTimer3(value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public string SetItemName + { + get + { + if (SetItem?.Deleted != false) + { + return null; + } + + return SetItem.Name; + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public Item SetItem { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public string MobTriggerProp { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public string MobTriggerName { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public Mobile MobTriggerId + { + get + { + if (MobTriggerName == null) + { + return null; + } + + // try to parse out the type information if it has also been saved + var typeargs = MobTriggerName.Split(",".ToCharArray(), 2); + string typestr = null; + var namestr = MobTriggerName; + + if (typeargs.Length > 1) + { + namestr = typeargs[0]; + typestr = typeargs[1]; + } + return BaseXmlSpawner.FindMobileByName(this, namestr, typestr); + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public string PlayerTriggerProp { get; set; } + + // time of day activation + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan TODStart { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan TODEnd { get; set; } + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan TOD + { + get + { + if (TODMode == TODModeType.Gametime) + { + Clock.GetTime(Map, Location.X, Location.Y, out var hours, out int minutes); + return new DateTime(Core.Now.Year, Core.Now.Month, Core.Now.Day, hours, minutes, 0).TimeOfDay; + } + + return Core.Now.TimeOfDay; + } + + } + + [CommandProperty(AccessLevel.GameMaster)] + public TODModeType TODMode { get; set; } = TODModeType.Realtime; + + [CommandProperty(AccessLevel.GameMaster)] + public bool TODInRange + { + get + { + if (TODStart == TODEnd) + { + return true; + } + + DateTime now; + + if (TODMode == TODModeType.Gametime) + { + Clock.GetTime(Map, Location.X, Location.Y, out var hours, out int minutes); + now = new DateTime(Core.Now.Year, Core.Now.Month, Core.Now.Day, hours, minutes, 0); + } + else + { + // calculate the time window + now = Core.Now; + } + var day_start = new DateTime(now.Year, now.Month, now.Day); + // calculate the starting TOD window by adding the TODStart to day_start + var TOD_start = day_start + TODStart; + var TOD_end = day_start + TODEnd; + + // handle the case when TODstart is before midnight and end is after + + if (TOD_start > TOD_end) + { + if (now > TOD_start || now < TOD_end) + { + return true; + } + + return false; + } + + if (now > TOD_start && now < TOD_end) + { + return true; + } + + return false; + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan DespawnTime { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan Duration + { + get => m_Duration; + set + { + m_Duration = value; + InvalidateProperties(); + } + } + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan DurationOver + { + get + { + if (m_durActivated) + { + return m_DurEnd - Core.Now; + } + + return TimeSpan.FromSeconds(0); + } + set => DoTimer2(value); + } + // proximity range parameter + [CommandProperty(AccessLevel.GameMaster)] + public int ProximityRange + { + get => m_ProximityRange; + set + { + m_ProximityRange = value; + InvalidateProperties(); + } + } + + // proximity range activated? + [CommandProperty(AccessLevel.GameMaster)] + public bool ProximityActivated + { + get => m_proximityActivated; + set + { + + if (AllowTriggering) + { + ActivateTrigger(); + } + + m_proximityActivated = value; + + } + } + + // proximity trigger sound parameter + [CommandProperty(AccessLevel.GameMaster)] + public int ProximitySound { get; set; } + + // proximity trigger message parameter + [CommandProperty(AccessLevel.GameMaster)] + public string ProximityMsg { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public string SpeechTrigger { get; set; } + + public string SkillTrigger { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan NextSpawn + { + get + { + if (m_Running) + { + return m_End - Core.Now; + } + + return TimeSpan.FromSeconds(0); + } + set + { + Start(); + DoTimer(value); + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool SpawnOnTrigger { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool Group + { + get => m_Group; + set { m_Group = value; InvalidateProperties(); } + } + + [CommandProperty(AccessLevel.GameMaster)] + public string GumpState { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public int SequentialSpawn { get; set; } = -1; + + [CommandProperty(AccessLevel.GameMaster)] + public TimeSpan NextSeqReset + { + get + { + if (m_Running && m_SeqEnd - Core.Now > TimeSpan.Zero) + { + return m_SeqEnd - Core.Now; + } + + return TimeSpan.FromSeconds(0); + } + set => m_SeqEnd = Core.Now + value; + } + + [CommandProperty(AccessLevel.GameMaster)] + public AccessLevel TriggerAccessLevel { get; set; } = AccessLevel.Player; + + [CommandProperty(AccessLevel.GameMaster)] + public bool DoRespawn + { + get => false; + set + { + // need to determine whether this is being set by the spawner during processing of a respawn entry + // if so then dont do it, otherwise you will infinitely recurse and crash with a stack overflow + if (value && !inrespawn) + { + _ = TryRespawn(); + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool DoReset + { + get => false; + set + { + if (value) + { + Reset(); + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool AllowGhostTrig { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool AllowNPCTrig { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public string ConfigFile { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool LoadConfig + { + get => false; + set + { + if (value) + { + LoadXmlConfig(ConfigFile); + } + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public Mobile TriggerMob { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool SmartSpawning + { + get => m_SmartSpawning; + set + { + m_SmartSpawning = value; + + if (m_SmartSpawning) + { + // if any spawner is smartspawning, then the smartspawning system is enabled + SmartSpawningSystemEnabled = true; + // check to see if the global sector timer is running + if (m_GlobalSectorTimer?.Running != true) + { + // start the global smartspawning timer + DoGlobalSectorTimer(TimeSpan.FromSeconds(1)); + } + } + + //IsInactivated = false; + } + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool IsEmpty + { + get + { + if (m_SpawnObjects == null) + { + return true; + } + + foreach (var so in m_SpawnObjects) + { + if (so.SpawnedObjects?.Count > 0) + { + if (so.SpawnedObjects[0] is Mobile) + { + return false; + } + } + + } + return true; + } + } + + public Guid Guid { get; } + public bool UnlinkOnTaming => true; + + [CommandProperty(AccessLevel.Developer)] + public bool ReturnOnDeactivate { get; set; } + + public Point3D HomeLocation => Location; + public int Range => HomeRange; + + public virtual void GetSpawnProperties(ISpawnable spawn, IPropertyList list) + { } + + public virtual void GetSpawnContextEntries(ISpawnable spawn, Mobile user, List list) + { } + + public void Remove(ISpawnable spawn) + { + if (m_SpawnObjects == null) + { + return; + } + + foreach (var so in m_SpawnObjects) + { + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + if (so.SpawnedObjects[i] == spawn) + { + _ = so.SpawnedObjects.Remove(spawn); + if (SequentialSpawn >= 0 && so.RestrictKillsToSubgroup) + { + if (so.SubGroup == SequentialSpawn) + { + m_killcount++; + } + } + else + { + m_killcount++; + } + + return; + } + } + } + } + + public void RestoreISpawner() + { + // restore the Spawner assignments to all spawned objects + if (m_SpawnObjects == null) + { + return; + } + + foreach (var so in m_SpawnObjects) + { + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + if (o is Item item) + { + item.Spawner = this; + } + else if (o is Mobile mobile) + { + mobile.Spawner = this; + } + } + } + } + + public override void OnAfterDuped(Item newItem) + { + ((XmlSpawner)newItem).Running = false; // automatically turn off duped spawners + + base.OnAfterDuped(newItem); + } + + public override void OnMapChange() + { + base.OnMapChange(); + + currentmap = Map; + + ResetSectorList(); // reset the sector list for smart spawning + } + + public override void OnDoubleClick(Mobile from) + { + if (from?.Deleted != false || from.AccessLevel < AccessLevel.GameMaster || SpawnerGump != null && SomeOneHasGumpOpen) + { + return; + } + + DeleteTextEntryBook(); // clear any text entry books that might still be around + + var x = 0; + var y = 0; + + // read the text entries for default values + + if (from.Account is Account acct) + { + var defs = XmlSpawnerDefaults.GetDefaults(acct.ToString(), from.Name); + if (defs != null) + { + x = defs.SpawnerGumpX; + y = defs.SpawnerGumpY; + } + } + + var g = new XmlSpawnerGump(this, x, y, 0, 0, 0); + _ = from.SendGump(g); + } + + public override void GetProperties(IPropertyList list) + { + base.GetProperties(list); + + list.Add(m_Running ? 1060742 : 1060743); // Active - Inactive + + // add whitespace to the beginning to avoid any problem with names that begin with # and are interpreted as cliloc ids + list.Add(1042971, $" {Name}"); // ~1_val~ + list.Add(1060656, m_Count.ToString()); // amount to make: ~1_val~ + list.Add(1061169, m_HomeRange.ToString()); // range ~1_val~ + + var nlist_items = 6; + + if (m_Group) + { + list.Add(1060658 + 6 - nlist_items, $"{"group"}\t{m_Group}"); // ~1_val~: ~2_val~ + nlist_items--; + } + + if (m_Team != 0) + { + list.Add(1060658 + 6 - nlist_items, $"{"team"}\t{m_Team}"); // ~1_val~: ~2_val~ + nlist_items--; + } + + list.Add(1060658 + 6 - nlist_items, $"{"speed"}\t{m_MinDelay} to {m_MaxDelay}"); // ~1_val~: ~2_val~ + nlist_items--; + + // display the duration parameter in the prop gump if it is non-zero + if (m_Duration > TimeSpan.FromMinutes(0)) + { + list.Add(1060658 + 6 - nlist_items, $"{"Duration"}\t{m_Duration}"); + nlist_items--; + } + + // display the proximity range parameter in the prop gump if it is active + if (m_ProximityRange != -1) + { + list.Add(1060658 + 6 - nlist_items, $"{"ProximityRange"}\t{m_ProximityRange}"); + nlist_items--; + } + + if (m_SpawnObjects != null) + { + for (var i = 0; i < nlist_items && i < m_SpawnObjects.Count; ++i) + { + var typename = m_SpawnObjects[i].TypeName; + if (typename?.Length > 20) + { + typename = typename[..20]; + } + + typename = $" {typename}"; + + list.Add(1060658 + (6 - nlist_items) + i, $"{typename}\t{m_SpawnObjects[i].SpawnedObjects.Count}"); + } + } + } + + public override void OnDelete() + { + base.OnDelete(); + + if (ShowBounds) + { + ShowBounds = false; + } + + RemoveSpawnObjects(); + + // remove any text entry books that might still be attached to the spawner + DeleteTextEntryBook(); + + m_Timer?.Stop(); + + m_DurTimer?.Stop(); + + m_RefractoryTimer?.Stop(); + + // if statics were added for marking container held spawners, delete them + if (m_ShowContainerStatic?.Deleted == false) + { + m_ShowContainerStatic.Delete(); + } + } + + private bool IgnoreLocationChange; + + public override void OnLocationChange(Point3D oldLocation) + { + if (IgnoreLocationChange) + { + IgnoreLocationChange = false; + return; + } + + // calculate the positional shift + if (oldLocation.X > 0 && oldLocation.Y > 0) + { + var diffx = X - oldLocation.X; + var diffy = Y - oldLocation.Y; + m_X += diffx; + m_Y += diffy; + } + else + { + // Keep the original dimensions the same (Width, Height), + // just recalculate the new top left corner + m_X = X - m_Width / 2; + m_Y = Y - m_Height / 2; + } + + // reset the sector list for smart spawning + ResetSectorList(); + + // Check if the spawner is showing its bounds + if (ShowBounds) + { + ShowBounds = false; + ShowBounds = true; + } + } + + public static bool SomeOneHasGumpOpen + { + get + { + // go through all online mobiles and see if any have xmlspawner gumps open + var states = TcpServer.Instances; + + foreach (var ns in states) + { + var m = ns.Mobile; + if (m != null && m.HasGump()) + { + return true; + } + } + + return false; + } + } + + public static void SpawnerGumpCallback(Mobile from, object invoker, string response) + { + // assign the response to the gumpstate + if (invoker is XmlSpawner xs) + { + xs.GumpState = response; + } + } + + public void DeleteTextEntryBook() + { + if (m_TextEntryBook != null) + { + foreach (var s in m_TextEntryBook) + { + s.Delete(); + } + + m_TextEntryBook = null; + } + } + + private static bool IsConstructible(ConstructorInfo ctor) => ctor.IsDefined(typeof(ConstructibleAttribute), false); + + public static int ConvertToInt(string value) => + value.StartsWith("0x") ? Convert.ToInt32(value.Substring(2), 16) : Convert.ToInt32(value); + + public static void ExecuteAction(object attachedto, Mobile trigmob, string action) + { + var loc = Point3D.Zero; + Map map = null; + if (attachedto is IEntity entity) + { + loc = entity.Location; + map = entity.Map; + } + + if (action == null || action.Length <= 0 || attachedto == null || map == null) + { + return; + } + + var TheSpawn = new SpawnObject(null, 0) + { + TypeName = action + }; + var substitutedtypeName = BaseXmlSpawner.ApplySubstitution(null, attachedto, action); + var typeName = BaseXmlSpawner.ParseObjectType(substitutedtypeName); + + if (BaseXmlSpawner.IsTypeOrItemKeyword(typeName)) + { + _ = BaseXmlSpawner.SpawnTypeKeyword(attachedto, TheSpawn, typeName, substitutedtypeName, trigmob, map, out _); + } + else + { + // its a regular type descriptor so find out what it is + var type = AssemblyHandler.FindTypeByName(typeName); + try + { + var arglist = BaseXmlSpawner.ParseString(substitutedtypeName, 3, "/"); + var o = CreateObject(type, arglist[0]); + + if (o is Mobile mobile) + { + if (mobile is BaseCreature creature) + { + creature.Home = loc; // Spawners location is the home point + } + + mobile.Location = loc; + mobile.Map = map; + + _ = BaseXmlSpawner.ApplyObjectStringProperties(null, substitutedtypeName, mobile, trigmob, attachedto, out _); + } + else if (o is Item item) + { + BaseXmlSpawner.AddSpawnItem(null, attachedto, TheSpawn, item, loc, map, trigmob, false, substitutedtypeName, out _); + } + } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + } + } + + private static void RemoveFromSectorTable(Sector s, XmlSpawner spawner) + { + if (s == null || s.Owner == null || s.Owner == Map.Internal || GlobalSectorTable[s.Owner.MapID] == null) + { + return; + } + + // find the sector + if (GlobalSectorTable[s.Owner.MapID].TryGetValue(s, out var spawnerlist) && spawnerlist != null) + { + //List spawnerlist = GlobalSectorTable[s.Owner.MapID][s]; + if (spawnerlist.Contains(spawner)) + { + _ = spawnerlist.Remove(spawner); + } + } + } + + private void ResetSectorList() + { + // remove the global sector entries + if (sectorList != null) + { + foreach (var s in sectorList) + { + RemoveFromSectorTable(s, this); + + } + } + sectorList = null; + SingleSector = false; + + // force an update of the sector list + _ = HasActiveSectors; + } + + public void LoadXmlConfig(string filename) + { + if (filename == null || filename.Length <= 0) + { + return; + } + + // Check if the file exists + if (File.Exists(filename)) + { + FileStream fs = null; + try + { + fs = File.Open(filename, FileMode.Open, FileAccess.Read); + } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + + if (fs == null) + { + status_str = $"Unable to open {filename} for loading"; + return; + } + + // Create the data set + var ds = new DataSet(XmlDataSetName); + + // Read in the file + var fileerror = false; + try + { + _ = ds.ReadXml(fs); + } + catch { fileerror = true; } + // close the file + fs.Close(); + if (fileerror) + { + Console.WriteLine("XmlSpawner: Error in XML config file '{0}'", filename); + return; + } + + // Check that at least a single table was loaded + if (ds.Tables.Count > 0) + { + if (ds.Tables[XmlTableName]?.Rows.Count > 0) + { + foreach (DataRow dr in ds.Tables[XmlTableName].Rows) + { + string strEntry = null; + var boolEntry = true; + double doubleEntry = 0; + var intEntry = 0; + + var valid_entry = true; + try { strEntry = (string)dr["Name"]; } + catch { valid_entry = false; } + if (valid_entry) { Name = strEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["X"]); } + catch { valid_entry = false; } + if (valid_entry) { m_X = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["Y"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Y = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["Width"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Width = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["Height"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Height = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["CentreX"]); } + catch { valid_entry = false; } + if (valid_entry) { X = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["CentreY"]); } + catch { valid_entry = false; } + if (valid_entry) { Y = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["CentreZ"]); } + catch { valid_entry = false; } + if (valid_entry) { Z = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["SequentialSpawning"]); } + catch { valid_entry = false; } + if (valid_entry) { SequentialSpawn = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["ProximityRange"]); } + catch { valid_entry = false; } + if (valid_entry) { m_ProximityRange = intEntry; } + + valid_entry = true; + try { strEntry = (string)dr["ProximityTriggerMessage"]; } + catch { valid_entry = false; } + if (valid_entry) { ProximityMsg = strEntry; } + + valid_entry = true; + try { strEntry = (string)dr["SpeechTrigger"]; } + catch { valid_entry = false; } + if (valid_entry) { SpeechTrigger = strEntry; } + + valid_entry = true; + try { strEntry = (string)dr["SkillTrigger"]; } + catch { valid_entry = false; } + if (valid_entry) { SkillTrigger = strEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["ProximityTriggerSound"]); } + catch { valid_entry = false; } + if (valid_entry) { ProximitySound = intEntry; } + + valid_entry = true; + try { strEntry = (string)dr["ItemTriggerName"]; } + catch { valid_entry = false; } + if (valid_entry) { m_ItemTriggerName = strEntry; } + + valid_entry = true; + try { strEntry = (string)dr["NoItemTriggerName"]; } + catch { valid_entry = false; } + if (valid_entry) { m_NoItemTriggerName = strEntry; } + + // check for the delayinsec entry + var delayinsec = false; + try { delayinsec = bool.Parse((string)dr["DelayInSec"]); } + catch { } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["MinDelay"]); } + catch { valid_entry = false; } + if (valid_entry) + { + m_MinDelay = delayinsec ? TimeSpan.FromSeconds(doubleEntry) : TimeSpan.FromMinutes(doubleEntry); + } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["MaxDelay"]); } + catch { valid_entry = false; } + if (valid_entry) + { + m_MaxDelay = delayinsec ? TimeSpan.FromSeconds(doubleEntry) : TimeSpan.FromMinutes(doubleEntry); + } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["Duration"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Duration = TimeSpan.FromMinutes(doubleEntry); } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["DespawnTime"]); } + catch { valid_entry = false; } + if (valid_entry) { DespawnTime = TimeSpan.FromHours(doubleEntry); } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["MinRefractory"]); } + catch { valid_entry = false; } + if (valid_entry) { RefractMin = TimeSpan.FromMinutes(doubleEntry); } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["MaxRefractory"]); } + catch { valid_entry = false; } + if (valid_entry) { RefractMax = TimeSpan.FromMinutes(doubleEntry); } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["TODStart"]); } + catch { valid_entry = false; } + if (valid_entry) { TODStart = TimeSpan.FromMinutes(doubleEntry); } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["TODEnd"]); } + catch { valid_entry = false; } + if (valid_entry) { TODEnd = TimeSpan.FromMinutes(doubleEntry); } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["TODMode"]); } + catch { valid_entry = false; } + if (valid_entry) { TODMode = (TODModeType)intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["Amount"]); } + catch { valid_entry = false; } + if (valid_entry) { StackAmount = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["MaxCount"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Count = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["Range"]); } + catch { valid_entry = false; } + if (valid_entry) { m_HomeRange = intEntry; } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["Team"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Team = intEntry; } + + valid_entry = true; + try { strEntry = (string)dr["WayPoint"]; } + catch { valid_entry = false; } + if (valid_entry) { WayPoint = GetWaypoint(strEntry); } + + valid_entry = true; + try { intEntry = int.Parse((string)dr["KillReset"]); } + catch { valid_entry = false; } + if (valid_entry) { KillReset = intEntry; } + + valid_entry = true; + try { doubleEntry = double.Parse((string)dr["TriggerProbability"]); } + catch { valid_entry = false; } + if (valid_entry) { TriggerProbability = doubleEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["ExternalTriggering"]); } + catch { valid_entry = false; } + if (valid_entry) { ExternalTriggering = boolEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["IsGroup"]); } + catch { valid_entry = false; } + if (valid_entry) { m_Group = boolEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["IsHomeRangeRelative"]); } + catch { valid_entry = false; } + if (valid_entry) { HomeRangeIsRelative = boolEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["AllowGhostTriggering"]); } + catch { valid_entry = false; } + if (valid_entry) { AllowGhostTrig = boolEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["AllowNPCTriggering"]); } + catch { valid_entry = false; } + if (valid_entry) { AllowNPCTrig = boolEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["SpawnOnTrigger"]); } + catch { valid_entry = false; } + if (valid_entry) { SpawnOnTrigger = boolEntry; } + + valid_entry = true; + try { boolEntry = bool.Parse((string)dr["SmartSpawning"]); } + catch { valid_entry = false; } + if (valid_entry) { m_SmartSpawning = boolEntry; } + + valid_entry = true; + try { strEntry = (string)dr["RegionName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + RegionName = strEntry; + } + + valid_entry = true; + try { strEntry = (string)dr["PlayerPropertyName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + PlayerTriggerProp = strEntry; + } + + valid_entry = true; + try { strEntry = (string)dr["MobPropertyName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + MobTriggerProp = strEntry; + } + + valid_entry = true; + try { strEntry = (string)dr["MobTriggerName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + MobTriggerName = strEntry; + } + + valid_entry = true; + try { strEntry = (string)dr["ObjectPropertyName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + m_ObjectPropertyName = strEntry; + } + + valid_entry = true; + try { strEntry = (string)dr["ObjectPropertyItemName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + var typeargs = strEntry.Split(",".ToCharArray(), 2); + string typestr = null; + var namestr = strEntry; + + if (typeargs.Length > 1) + { + namestr = typeargs[0]; + typestr = typeargs[1]; + } + m_ObjectPropertyItem = BaseXmlSpawner.FindItemByName(this, namestr, typestr); + } + + valid_entry = true; + try { strEntry = (string)dr["SetPropertyItemName"]; } + catch { valid_entry = false; } + if (valid_entry) + { + var typeargs = strEntry.Split(",".ToCharArray(), 2); + string typestr = null; + var namestr = strEntry; + + if (typeargs.Length > 1) + { + namestr = typeargs[0]; + typestr = typeargs[1]; + } + SetItem = BaseXmlSpawner.FindItemByName(this, namestr, typestr); + } + + valid_entry = true; + try { strEntry = (string)dr["Name"]; } + catch { valid_entry = false; } + if (valid_entry) { Name = strEntry; } + + valid_entry = true; + try { strEntry = (string)dr["Map"]; } + catch { valid_entry = false; } + if (valid_entry) + { + // Convert the xml map value to a real map object + try + { + Map = Map.Parse(strEntry); + } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + } + + // try loading the new spawn specifications first + var Spawns = Array.Empty(); + var havenew = true; + valid_entry = true; + try { Spawns = SpawnObject.LoadSpawnObjectsFromString2((string)dr["Objects2"]); } + catch { havenew = false; } + if (!havenew) + { + // try loading the new spawn specifications + try { Spawns = SpawnObject.LoadSpawnObjectsFromString((string)dr["Objects"]); } + catch { valid_entry = false; } + // can only have one of these defined + } + if (valid_entry) + { + + // clear existing spawns + RemoveSpawnObjects(); + + // Create the new array of spawned objects + m_SpawnObjects = new List(); + + // Assign the list of objects to spawn + SpawnObjects = Spawns; + } + } + } + } + } + } + + public void ReportStatus() + { + if (PropertyInfoList != null) + { + Console.WriteLine("PropertyInfoList: {0}", PropertyInfoList.Count); + foreach (var to in PropertyInfoList) + { + Console.WriteLine("\t{0}", to.t); + foreach (var p in to.plist) + { + Console.WriteLine("\t\t{0}", p); + } + } + } + + ShowTagList(this); + var count = 0; + Console.WriteLine("Registered SkillsTotal = {0}", count); + } + +#if TRACE + + public static readonly string[] _traceName = + { + string.Empty, + "XmlFind", + "HasSector", + string.Empty, + "AttachSpeech", + "HasHold", + string.Empty, + string.Empty, + "OnTick", + "Defrag", + "Respawn", + "SetProp", + "AttachMovement", + "ActiveSector", + string.Empty, + "DistroTick", + "GetScaledFaction", + "FactionOnKill", + "CheckAcquire", + string.Empty, + }; + + private static readonly DateTime[] _traceStart = new DateTime[_traceName.Length]; + private static readonly TimeSpan[] _traceTotal = new TimeSpan[_traceName.Length]; + private static readonly int[] _traceCount = new int[_traceName.Length]; + + private static DateTime _traceStartTime = Core.Now; + private static double _startProcessTime; + + public static void TraceStart(int index) + { + if (index < _traceStart.Length) + { + _traceStart[index] = Core.Now; + } + } + public static void TraceEnd(int index) + { + if (index < _traceStart.Length) + { + _traceTotal[index] += Core.Now - _traceStart[index]; + _traceCount[index]++; + } + } +#else + public static void TraceStart(int index) { } + public static void TraceEnd(int index) { } +#endif + + private bool ValidPlayerTrig(Mobile m) + { + if (m?.Deleted != false) + { + return false; + } + + return (m.Player || AllowNPCTrig) && m.AccessLevel <= TriggerAccessLevel && (!m.Body.IsGhost && !AllowGhostTrig || m.Body.IsGhost && AllowGhostTrig); + } + + private bool AllowTriggering => m_Running && !m_refractActivated && TODInRange && CanSpawn; + + private void ActivateTrigger() + { + DoTimer(); // reset the timer + + // start the refractory timer to set proximity activated to false, thus enabling another activation + if (RefractMax > TimeSpan.FromMinutes(0)) + { + var minSeconds = (int)RefractMin.TotalSeconds; + var maxSeconds = (int)RefractMax.TotalSeconds; + + DoTimer3(TimeSpan.FromSeconds(Utility.RandomMinMax(minSeconds, maxSeconds))); + } + + // if the spawnontrigger flag is set, then spawn immediately + if (SpawnOnTrigger) + { + NextSpawn = TimeSpan.Zero; + ResetNextSpawnTimes(); + } + + // reset speech triggering if it was set + m_speechTriggerActivated = false; + } + + public void CheckTriggers(Mobile m, Skill s, bool hasproximity) + { + if (AllowTriggering && !m_proximityActivated) // only proximity trigger when no spawns have already been triggered + { + var needs_speech_trigger = false; + var needs_player_trigger = false; + var has_player_trigger = false; + + m_skipped = false; + + // test for the various triggering options in the order of increasing computational demand. No point checking a high demand test + // if a low demand one has already failed. + + // check for external triggering + if (ExternalTriggering && !ExtTrigState) + { + return; + } + + // if speech triggering is set then test for successful activation + if (!string.IsNullOrEmpty(SpeechTrigger)) + { + needs_speech_trigger = true; + } + // check to see if we have to continue + if (needs_speech_trigger && !m_speechTriggerActivated) + { + return; + } + + // if player property triggering is set then look for the mob and test properties + if (!string.IsNullOrEmpty(PlayerTriggerProp)) + { + needs_player_trigger = true; + + if (BaseXmlSpawner.TestMobProperty(this, m, PlayerTriggerProp, out var status_str)) + { + has_player_trigger = true; + } + + if (!string.IsNullOrEmpty(status_str)) + { + this.status_str = status_str; + } + } + + // check to see if we have to continue + if (needs_player_trigger && !has_player_trigger) + { + return; + } + + // if this was called without being proximity triggered then check to see that the non-movement triggers were enabled. + if (!hasproximity && !ExternalTriggering) + { + return; + } + + // all of the necessary trigger conditions have been met so go ahead and trigger + // after you make the probability check + + if (Utility.RandomDouble() < TriggerProbability) + { + // play a sound indicating the spawner has been triggered + if (ProximitySound > 0 && m?.Deleted == false) + { + m.PlaySound(ProximitySound); + } + + // display the trigger message + if (!string.IsNullOrEmpty(ProximityMsg) && m?.Deleted == false) + { + m.PublicOverheadMessage(MessageType.Regular, 0x3B2, false, ProximityMsg); + } + + // enable spawning at the next ontick + // this will also start the refractory timer and send the triggering indicators + ProximityActivated = true; + + // keep track of who triggered this + TriggerMob = m; + } + else + { + m_skipped = true; + + // reset speech triggering if it was set + m_speechTriggerActivated = false; + } + } + } + + public bool HandlesOnSkillUse => m_Running && SkillTrigger?.Length > 0; + + // this is the handler for skill use + public void OnSkillUse(Mobile m, Skill skill, bool success) + { + if (m_Running && m_ProximityRange >= 0 && ValidPlayerTrig(m) && CanSpawn && !m_refractActivated && TODInRange) + { + if (!Utility.InRange(m.Location, Location, m_ProximityRange)) + { + return; + } + + // m_skillTriggerActivated = false; + + // check the skill trigger conditions, Skillname[+/-][,min,max] + // if (m_SkillTrigger != null && skill.SkillName == m_SkillTriggerName && + // (m_SkillTriggerMin < 0 || skill.Value >= m_SkillTriggerMin) && + // (m_SkillTriggerMax < 0 || skill.Value <= m_SkillTriggerMax) && + // (m_SkillTriggerSuccess == 3 || m_SkillTriggerSuccess == 1 && success || m_SkillTriggerSuccess == 2 && !success)) + // { + // // have a skill trigger so flag it and test it + // m_skillTriggerActivated = true; + // + // CheckTriggers(m, skill, true); + // } + } + } + + public override bool HandlesOnSpeech => m_Running && !string.IsNullOrEmpty(SpeechTrigger); + + public override void OnSpeech(SpeechEventArgs e) + { + if (m_Running && m_ProximityRange >= 0 && ValidPlayerTrig(e.Mobile) && CanSpawn && !m_refractActivated && TODInRange) + { + m_speechTriggerActivated = false; + + if (!Utility.InRange(e.Mobile.Location, Location, m_ProximityRange)) + { + return; + } + + if (SpeechTrigger != null && e.Speech.ToLower().IndexOf(SpeechTrigger.ToLower()) >= 0) + { + e.Handled = true; + + // found the speech trigger so flag it for testing in the onmovement handler where the other proximity features are tested + m_speechTriggerActivated = true; + + CheckTriggers(e.Mobile, null, true); + } + } + } + + public override bool HandlesOnMovement => m_Running && m_ProximityRange >= 0; + + public void AddToMovementList(Mobile m) + { + // go through the list and check for redundancy + m_MovementList ??= new List(); + + // check to see if the movement timer is running + if (m_MovementTimer?.Running != true) + { + DoMovementTimer(TimeSpan.FromSeconds(1)); + } + + var add = true; + + foreach (var moveinfo in m_MovementList) + { + if (moveinfo.trigMob == m) + { + add = false; + break; + } + } + + // wasnt on the list so add it + if (add) + { + // is the list at max throttling length? + if (m_MovementList.Count > MaxMoveCheck) + { + // replace a random entry in the current list with this one + m_MovementList[Utility.Random(m_MovementList.Count)] = new MovementInfo(m); + } + else + { + + m_MovementList.Add(new MovementInfo(m)); + } + } + } + + public void DoMovementTimer(TimeSpan delay) + { + m_MovementTimer?.Stop(); + + m_MovementTimer = new MovementTimer(this, delay); + + _ = m_MovementTimer.Start(); + } + + private class MovementTimer : Timer + { + private readonly XmlSpawner m_Spawner; + + public MovementTimer(XmlSpawner spawner, TimeSpan delay) : base(delay) => m_Spawner = spawner; + + protected override void OnTick() + { + // check everyone on the movement list then clear the list + if (m_Spawner?.Deleted == false) + { + if (m_Spawner.m_Running && !m_Spawner.m_proximityActivated && !m_Spawner.m_refractActivated && m_Spawner.TODInRange && m_Spawner.CanSpawn) + { + var count = 0; + var maxspeed = 0; + + foreach (var moveinfo in m_Spawner.m_MovementList) + { + var m = moveinfo.trigMob; + + if (m == null) + { + continue; + } + + // additional throttling in here by limiting number of mobs that can be checked in a single ontick + count++; + + if (count > MaxMoveCheck) + { + break; + } + + var speed = (int)GetDistance(m.Location, moveinfo.trigLocation); + + if (speed > maxspeed) + { + maxspeed = speed; + } + + m_Spawner.CheckTriggers(m, null, true); + } + + m_Spawner.MovingPlayerCount = m_Spawner.m_MovementList.Count; + m_Spawner.FastestPlayerSpeed = maxspeed; + + } + + m_Spawner.m_MovementList.Clear(); + } + } + } + + public static double GetDistance(Point3D p1, Point3D p2) + { + var xDelta = p1.X - p2.X; + var yDelta = p1.Y - p2.Y; + + return Math.Sqrt(xDelta * xDelta + yDelta * yDelta); + } + + public override void OnMovement(Mobile m, Point3D oldLocation) + { + if (m_Running && m_ProximityRange >= 0 && ValidPlayerTrig(m) && CanSpawn) + { + // check to see if player is within range of the spawner + if (Parent == null && Utility.InRange(m.Location, Location, m_ProximityRange)) + { + // add some throttling code here. + // add the player to a list that gets cleared every few seconds, checking for redundancy then trigger off of the list instead of off of + // the actual movement stream + + AddToMovementList(m); + } + else + { + // clear any speech triggering + m_speechTriggerActivated = false; + } + } + + base.OnMovement(m, oldLocation); + } + + public static bool AssignSettings(string argname, string value) + { + switch (argname) + { + case "XmlSpawnDir": + { + XmlSpawnDir = value; + break; + } + case "DiskAccessLevel": + { + DiskAccessLevel = Enum.Parse(value, true); + break; + } + case "SmartSpawnAccessLevel": + { + SmartSpawnAccessLevel = Enum.Parse(value, true); + break; + } + case "defaultTriggerSound": + { + defaultTriggerSound = ConvertToInt(value); + defProximityTriggerSound = defaultTriggerSound; + break; + } + case "BaseItemId": + { + BaseItemId = ConvertToInt(value); + break; + } + case "ShowItemId": + { + ShowItemId = ConvertToInt(value); + break; + } + case "MaxMoveCheck": + { + MaxMoveCheck = ConvertToInt(value); + break; + } + case "defMinDelay": + { + defMinDelay = TimeSpan.FromMinutes(ConvertToInt(value)); + break; + } + case "defMaxDelay": + { + defMaxDelay = TimeSpan.FromMinutes(ConvertToInt(value)); + break; + } + case "defRelativeHome": + { + defRelativeHome = bool.Parse(value); + break; + } + case "defSpawnRange": + { + defSpawnRange = ConvertToInt(value); + break; + } + case "defHomeRange": + { + defHomeRange = ConvertToInt(value); + break; + } + case "BlockKeyword": + { + // parse the keyword list and remove them from the keyword hashtables + var keywordlist = value.Split(','); + + if (keywordlist.Length > 0) + { + for (var i = 0; i < keywordlist.Length; i++) + { + BaseXmlSpawner.RemoveKeyword(keywordlist[i]); + } + } + + break; + } + case "BlockCommand": + case "ChangeCommand": + { + // delay processing of these settings until after all commands have been registered in their Initialize methods + _ = Timer.DelayCall(TimeSpan.Zero, DelayedAssignSettings, argname, value); + break; + } + default: + { + return false; + } + } + + return true; + } + + private static void DelayedAssignSettings(string argname, string value) + { + switch (argname) + { + case "BlockCommand": + { + // delay processing of this until after all commands have been registered in their Initialize methods + // parse the command list and remove them from the command hashtables + // the syntax is "commandname, commandname, etc." + var keywordlist = value.Split(','); + + if (keywordlist.Length > 0) + { + for (var i = 0; i < keywordlist.Length; i++) + { + var commandname = keywordlist[i].Trim().ToLower(); + try + { + _ = CommandSystem.Entries.Remove(commandname); + } + catch + { + Console.WriteLine("{0}: invalid command {1}", argname, commandname); + } + } + } + break; + } + case "ChangeCommand": + { + // delay processing of this until after all commands have been registered in their Initialize methods + // parse the command list and rehash them into the command hashtables + // the syntax is "oldname:newname[:accesslevel], oldname:newname[:accesslevel], etc." + var keywordlist = value.Split(','); + + if (keywordlist.Length > 0) + { + for (var i = 0; i < keywordlist.Length; i++) + { + var namelist = keywordlist[i].Split(':'); + if (namelist.Length > 1) + { + var oldname = namelist[0].Trim().ToLower(); + var newname = namelist[1].Trim(); + + if (newname.Length == 0) + { + newname = oldname; + } + + var access = AccessLevel.Player; + var validaccess = false; + if (namelist.Length > 2) + { + // get the new accesslevel + try + { + access = (AccessLevel)Enum.Parse(typeof(AccessLevel), namelist[2].Trim(), true); + validaccess = true; + } + catch + { + Console.WriteLine("{0}: invalid accesslevel {1} for {2}", argname, namelist[2], newname); + } + } + // find the command entry for the old name + CommandEntry e = null; + try + { + e = CommandSystem.Entries[oldname]; + } + catch + { + Console.WriteLine("{0}: invalid command {1}", argname, oldname); + } + if (e != null) + { + if (!validaccess) + { + // use the old accesslevel + access = e.AccessLevel; + } + // remove the old command entry + _ = CommandSystem.Entries.Remove(oldname); + // register the new command using the old handler + CommandSystem.Register(newname, access, e.Handler); + } + + // also look in the targetcommands list and adjust name and accesslevel there + foreach (var b in TargetCommands.AllCommands) + { + if (b.Commands != null) + { + for (var j = 0; j < b.Commands.Length; j++) + { + var commandname = b.Commands[j]; + if (commandname.ToLower() == oldname) + { + // modify the basecommand with the new name and access + b.Commands[j] = newname; + if (validaccess) + { + b.AccessLevel = access; + } + + // re-register it in the implementors hashtable + var impls = BaseCommandImplementor.Implementors; + + for (var k = 0; k < impls.Count; ++k) + { + var impl = impls[k]; + + if ((b.Supports & impl.SupportRequirement) != 0) + { + try + { + _ = impl.Commands.Remove(commandname); + } + catch (Exception ex) + { + Diagnostics.ExceptionLogging.LogException(ex); + } + impl.Register(b); + } + } + + break; + } + } + } + } + } + } + } + break; + } + } + } + + public delegate bool AssignSettingsHandler(string argname, string value); + + // load in settings from the xmlspawner2.cfg file in the Data directory + public static void LoadSettings(AssignSettingsHandler settingshandler, string section) + { + // Check if the file exists + var path = Path.Combine(Core.BaseDirectory, "Data/xmlspawner.cfg"); + + if (!File.Exists(path)) + { + return; + } + + Console.WriteLine("Loading {0} configuration", section); + using var ip = new StreamReader(path); + string line; + string currentsection = null; + var nsettings = 0; + + while ((line = ip.ReadLine()) != null) + { + line = line.Trim(); + + // skip comments + if (line.Length == 0 || line.StartsWith("#")) + { + continue; + } + + if (line.StartsWith("[")) + { + // parse the section name + var args = line.Split("[]".ToCharArray(), 3); + if (args.Length > 2) + { + currentsection = args[1].Trim(); + } + } + + // only process the matching classname section + if (currentsection != section) + { + continue; + } + + var split = line.Split('='); + + if (split.Length >= 2) + { + var argname = split[0].Trim(); + var value = split[1].Trim(); + + if (argname.Length == 0 || value.Length == 0) + { + continue; + } + + try + { + if (settingshandler(argname, value)) + { + nsettings++; + } + else + { + Console.WriteLine("'{0}' setting is invalid in section [{1}]", argname, currentsection); + } + } + catch (Exception e) + { + Console.WriteLine("Config error '{0}'='{1}'", argname, value); + Console.WriteLine("Error: {0}", e.Message); + Diagnostics.ExceptionLogging.LogException(e); + } + } + } + + if (nsettings > 0) + { + Console.WriteLine("{0} settings processed", nsettings); + } + } + + public static void Initialize() + { + LoadSettings(AssignSettings, "XmlSpawner"); + + // initialize the default waypoint name + var tmpwaypoint = new WayPoint(); + defwaypointname = tmpwaypoint.Name; + tmpwaypoint.Delete(); + + var count = 0; + var regional = 0; + + foreach (var item in World.Items.Values) + { + if (item is XmlSpawner spawner) + { + count++; + + if (!string.IsNullOrEmpty(spawner.RegionName)) + { + spawner.RegionName = spawner.RegionName; // invoke set(RegionName) + regional++; + } + + // check for smart spawning and restart timers after deser if needed + // note, HasActiveSectors will recalculate the sector list and UseSectorActivate property + var recalc_sectors = spawner.HasActiveSectors; + + spawner.RestoreISpawner(); + } + } + + // start the global smartspawning timer + if (SmartSpawningSystemEnabled) + { + DoGlobalSectorTimer(TimeSpan.FromSeconds(1)); + } + + // standard commands + CommandSystem.Register("XmlSpawnerShowAll", AccessLevel.Administrator, ShowSpawnPoints_OnCommand); + CommandSystem.Register("XmlSpawnerHideAll", AccessLevel.Administrator, HideSpawnPoints_OnCommand); + CommandSystem.Register("XmlSpawnerWipe", AccessLevel.Administrator, Wipe_OnCommand); + CommandSystem.Register("XmlSpawnerWipeAll", AccessLevel.Administrator, WipeAll_OnCommand); + CommandSystem.Register("XmlSpawnerLoad", DiskAccessLevel, Load_OnCommand); + CommandSystem.Register("XmlSpawnerSave", DiskAccessLevel, Save_OnCommand); + CommandSystem.Register("XmlSpawnerSaveAll", DiskAccessLevel, SaveAll_OnCommand); + CommandSystem.Register("XmlSpawnerRespawn", AccessLevel.Seer, Respawn_OnCommand); + CommandSystem.Register("XmlSpawnerRespawnAll", AccessLevel.Seer, RespawnAll_OnCommand); + + CommandSystem.Register("XmlShow", AccessLevel.Administrator, ShowSpawnPoints_OnCommand); + CommandSystem.Register("XmlHide", AccessLevel.Administrator, HideSpawnPoints_OnCommand); + CommandSystem.Register("XmlHome", AccessLevel.GameMaster, XmlHome_OnCommand); + CommandSystem.Register("XmlUnLoad", DiskAccessLevel, UnLoad_OnCommand); + CommandSystem.Register("XmlSpawnerUnLoad", DiskAccessLevel, UnLoad_OnCommand); + CommandSystem.Register("XmlLoad", DiskAccessLevel, Load_OnCommand); + CommandSystem.Register("XmlLoadHere", DiskAccessLevel, LoadHere_OnCommand); + CommandSystem.Register("XmlNewLoad", DiskAccessLevel, NewLoad_OnCommand); + CommandSystem.Register("XmlNewLoadHere", DiskAccessLevel, NewLoadHere_OnCommand); + CommandSystem.Register("XmlSave", DiskAccessLevel, Save_OnCommand); + CommandSystem.Register("XmlSaveAll", DiskAccessLevel, SaveAll_OnCommand); + CommandSystem.Register("XmlSaveOld", DiskAccessLevel, SaveOld_OnCommand); + CommandSystem.Register("XmlImportSpawners", DiskAccessLevel, XmlImportSpawners_OnCommand); + CommandSystem.Register("XmlImportMSF", DiskAccessLevel, XmlImportMSF_OnCommand); + CommandSystem.Register("XmlImportMap", DiskAccessLevel, XmlImportMap_OnCommand); + CommandSystem.Register("XmlDefaults", AccessLevel.Administrator, XmlDefaults_OnCommand); + CommandSystem.Register("XmlGet", AccessLevel.GameMaster, XmlGetValue_OnCommand); + CommandSystem.Register("OptimalSmartSpawning", AccessLevel.Administrator, OptimalSmartSpawning_OnCommand); + CommandSystem.Register("SmartStat", AccessLevel.GameMaster, SmartStat_OnCommand); + CommandSystem.Register("XmlGo", AccessLevel.GameMaster, SpawnEditorGo_OnCommand); + + TargetCommands.Register(new XmlSetCommand()); + TargetCommands.Register(new XmlSaveSingle()); + +#if TRACE + CommandSystem.Register("XmlMake", AccessLevel.Administrator, XmlMake_OnCommand); + CommandSystem.Register("XmlTrace", AccessLevel.Administrator, XmlTrace_OnCommand); + CommandSystem.Register("XmlResetTrace", AccessLevel.Administrator, XmlResetTrace_OnCommand); +#endif + + } + + [Usage("XmlGet property")] + [Description("Returns value of the property on the targeted object.")] + public static void XmlGetValue_OnCommand(CommandEventArgs e) + { + e.Mobile.Target = new GetValueTarget(e); + } + private class GetValueTarget : Target + { + private readonly CommandEventArgs m_e; + public GetValueTarget(CommandEventArgs e) + : base(30, false, TargetFlags.None) => + m_e = e; + + protected override void OnTarget(Mobile from, object targeted) + { + var pname = m_e.GetString(0); + var result = BaseXmlSpawner.GetPropertyValue(null, targeted, pname, out var ptype); + + // see if it was successful + if (ptype == null) + { + return; + } + from.SendMessage($"{result}"); + + } + } + + public class XmlSetCommand : BaseCommand + { + public XmlSetCommand() + { + AccessLevel = AccessLevel.Administrator; + Supports = CommandSupport.All; + Commands = new[] { "XmlSet" }; + ObjectTypes = ObjectTypes.Both; + Usage = "XmlSet "; + Description = "Sets a property value by name of a targeted object. Provides access to all public properties."; + } + + public override void Execute(CommandEventArgs e, object obj) + { + if (e.Length >= 2) + { + var result = BaseXmlSpawner.SetPropertyValue(null, obj, e.GetString(0), e.GetString(1)); + + if (result == "Property has been set.") + { + AddResponse(result); + } + else + { + LogFailure(result); + } + } + else + { + LogFailure("Format: XmlSet "); + } + } + } + + [Usage("TagList property")] + [Description("Lists the keyword taglist for a spawner")] + public static void ShowTagList_OnCommand(CommandEventArgs e) + { + e.Mobile.Target = new TagListTarget(); + } + + private class TagListTarget : Target + { + public TagListTarget() + : base(30, false, TargetFlags.None) + { + } + + protected override void OnTarget(Mobile from, object targeted) + { + if (targeted is XmlSpawner spawner) + { + ShowTagList(spawner); + } + } + } + + public static void ShowTagList(XmlSpawner spawner) + { + var count = 0; + Console.WriteLine("{0} tags", spawner.m_KeywordTagList.Count); + foreach (var tag in spawner.m_KeywordTagList) + { + count++; + Console.WriteLine("tag {0} : {1}", count, BaseXmlSpawner.TagInfo(tag)); + } + } + + // added in targeting for the [xmlhome command + private class XmlHomeTarget : Target + { + private readonly CommandEventArgs m_e; + public XmlHomeTarget(CommandEventArgs e) + : base(30, false, TargetFlags.None) => + m_e = e; + + protected override void OnTarget(Mobile from, object targeted) + { + XmlSpawner spawner = null; + + if (targeted is XmlSpawner xmlSpawner) + { + if (m_e.GetString(0) == "status") + { + xmlSpawner.ReportStatus(); + return; + } + } + + if (targeted is ISpawnable s) + { + spawner = s.Spawner as XmlSpawner; + } + + if (spawner == null) + { + from.SendMessage("Unable to find spawner for this object"); + return; + } + + // check to make sure it is still on the spawner + foreach (var so in spawner.m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + + if (o == targeted) + { + from.SendMessage($"{spawner.Location}"); + + if (m_e.GetString(0) == "go") + { + // make sure the spawner is not in a container. + if (spawner.Parent == null) + { + from.Location = new Point3D(spawner.Location); + from.Map = spawner.Map; + } + else + { + from.SendMessage("Spawner is in a container"); + } + } + else + if (m_e.GetString(0) == "send") + { + // make sure the spawner is not in a container. + if (spawner.Parent == null) + { + if (o is Item item) + { + item.Location = new Point3D(spawner.Location); + item.Map = spawner.Map; + } + if (o is Mobile mobile1) + { + mobile1.Location = new Point3D(spawner.Location); + mobile1.Map = spawner.Map; + } + } + else + { + from.SendMessage("Spawner is in a container"); + } + } + else if (m_e.GetString(0) == "gump") + { + spawner.OnDoubleClick(from); + } + + return; + } + } + } + } + } + + [Usage("XmlHome [go][gump][send]")] + [Description("Returns the coordinates of the spawner for the targeted object. Args: 'go' teleports to spawner, 'gump' opens spawner gump, 'send' sends mob home")] + public static void XmlHome_OnCommand(CommandEventArgs e) + { + e.Mobile.Target = new XmlHomeTarget(e); + } + + private static void XmlSaveDefaults(string filePath, Mobile m) + { + + if (string.IsNullOrEmpty(filePath)) + { + return; + } + + using (var op = new StreamWriter(filePath)) + { + var xml = new XmlTextWriter(op) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + + xml.WriteStartDocument(true); + + xml.WriteStartElement("XmlDefaults"); + + xml.WriteStartElement("defProximityRange"); + xml.WriteString(defProximityRange.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defTriggerProbability"); + xml.WriteString(defTriggerProbability.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defProximityTriggerSound"); + xml.WriteString(defProximityTriggerSound.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defMinRefractory"); + xml.WriteString(defMinRefractory.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defMaxRefractory"); + xml.WriteString(defMaxRefractory.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defTODStart"); + xml.WriteString(defTODStart.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defTODEnd"); + xml.WriteString(defTODEnd.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defStackAmount"); + xml.WriteString(defAmount.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defDuration"); + xml.WriteString(defDuration.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defIsGroup"); + xml.WriteString(defIsGroup.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defTeam"); + xml.WriteString(defTeam.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defRelativeHome"); + xml.WriteString(defRelativeHome.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defSpawnRange"); + xml.WriteString(defSpawnRange.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defHomeRange"); + xml.WriteString(defHomeRange.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defMinDelay"); + xml.WriteString(defMinDelay.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defMaxDelay"); + xml.WriteString(defMaxDelay.ToString()); + xml.WriteEndElement(); + xml.WriteStartElement("defTODMode"); + xml.WriteString(defTODMode.ToString()); + xml.WriteEndElement(); + + xml.WriteEndElement(); + + xml.Close(); + } + m.SendMessage($"defaults saved to {filePath}"); + } + + public static void XmlLoadDefaults(string filePath, Mobile m) + { + if (m?.Deleted != false) + { + return; + } + + if (!string.IsNullOrEmpty(filePath)) + { + + if (File.Exists(filePath)) + { + var doc = new XmlDocument(); + doc.Load(filePath); + + var root = doc["XmlDefaults"]; + LoadDefaults(root); + m.SendMessage($"defaults loaded successfully from {filePath}"); + } + else + { + m.SendMessage($"File {filePath} does not exist."); + } + } + } + + private static void LoadDefaults(XmlElement node) + { + + try { defProximityRange = int.Parse(node["defProximityRange"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defTriggerProbability = double.Parse(node["defTriggerProbability"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defProximityTriggerSound = int.Parse(node["defProximityTriggerSound"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defMinRefractory = TimeSpan.Parse(node["defMinRefractory"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defMaxRefractory = TimeSpan.Parse(node["defMaxRefractory"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defTODStart = TimeSpan.Parse(node["defTODStart"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defTODEnd = TimeSpan.Parse(node["defTODEnd"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defAmount = int.Parse(node["defStackAmount"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defDuration = TimeSpan.Parse(node["defDuration"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defIsGroup = bool.Parse(node["defIsGroup"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defTeam = int.Parse(node["defTeam"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defRelativeHome = bool.Parse(node["defRelativeHome"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defSpawnRange = int.Parse(node["defSpawnRange"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defHomeRange = int.Parse(node["defHomeRange"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defMinDelay = TimeSpan.Parse(node["defMinDelay"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + try { defMaxDelay = TimeSpan.Parse(node["defMaxDelay"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + var todmode = 0; + try { todmode = int.Parse(node["defTODMode"].InnerText); } + catch (Exception e) + { + Diagnostics.ExceptionLogging.LogException(e); + } + + defTODMode = todmode switch + { + (int)TODModeType.Realtime => TODModeType.Realtime, + (int)TODModeType.Gametime => TODModeType.Gametime, + _ => defTODMode + }; + } + + [Usage("XmlDefaults [defaultpropertyname value]")] + [Description("Returns or changes the default settings of the spawner.")] + public static void XmlDefaults_OnCommand(CommandEventArgs e) + { + var m = e.Mobile; + if (m?.Deleted != false) + { + return; + } + + if (e.Arguments.Length >= 1) + { + // leave open the possibility of just requesting display of a single property + if (e.Arguments.Length == 2) + { + if (e.Arguments[0].ToLower() == "save") + { + XmlSaveDefaults(e.Arguments[1], m); + } + else if (e.Arguments[0].ToLower() == "load") + { + XmlLoadDefaults(e.Arguments[1], m); + } + else if (e.Arguments[0].ToLower() == "maxdelay") + { + try + { + defMaxDelay = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"MaxDelay = {defMaxDelay}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "mindelay") + { + try + { + defMinDelay = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"MinDelay = {defMinDelay}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "spawnrange") + { + try + { + defSpawnRange = Convert.ToInt32(e.Arguments[1]); + m.SendMessage($"SpawnRange = {defSpawnRange}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "homerange") + { + try + { + defHomeRange = Convert.ToInt32(e.Arguments[1]); + m.SendMessage($"HomeRange = {defHomeRange}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "relativehome") + { + try + { + defRelativeHome = Convert.ToBoolean(e.Arguments[1]); + m.SendMessage($"RelativeHome = {defRelativeHome}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "proximitytriggersound") + { + try + { + defProximityTriggerSound = Convert.ToInt32(e.Arguments[1]); + m.SendMessage($"ProximityTriggerSound = {defProximityTriggerSound}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "proximityrange") + { + try + { + defProximityRange = Convert.ToInt32(e.Arguments[1]); + m.SendMessage($"ProximityRange = {defProximityRange}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "triggerprobability") + { + try + { + defTriggerProbability = Convert.ToDouble(e.Arguments[1]); + m.SendMessage($"TriggerProbability = {defTriggerProbability}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "todstart") + { + try + { + defTODStart = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"TODStart = {defTODStart}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "todend") + { + try + { + defTODEnd = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"TODEnd = {defTODEnd}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "stackamount") + { + try + { + defAmount = Convert.ToInt32(e.Arguments[1]); + m.SendMessage($"StackAmount = {defAmount}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "duration") + { + try + { + defDuration = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"Duration = {defDuration}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "group") + { + try + { + defIsGroup = Convert.ToBoolean(e.Arguments[1]); + m.SendMessage($"Group = {defIsGroup}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "team") + { + try + { + defTeam = Convert.ToInt32(e.Arguments[1]); + m.SendMessage($"Team = {defTeam}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "todmode") + { + try + { + var todmode = Convert.ToInt32(e.Arguments[1]); + defTODMode = todmode switch + { + (int)TODModeType.Gametime => TODModeType.Gametime, + (int)TODModeType.Realtime => TODModeType.Realtime, + _ => defTODMode + }; + m.SendMessage($"TODMode = {defTODMode}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "maxrefractory") + { + try + { + defMaxRefractory = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"MaxRefractory = {defMaxRefractory}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else if (e.Arguments[0].ToLower() == "minrefractory") + { + try + { + defMinRefractory = TimeSpan.FromMinutes(Convert.ToDouble(e.Arguments[1])); + m.SendMessage($"MinRefractory = {defMinRefractory}"); + } + catch { m.SendMessage($"invalid value : {e.Arguments[1]}"); } + } + else + { + m.SendMessage($"{e.Arguments[0]} : no such default value."); + } + } + + } + else + { + // just display the values + m.SendMessage($"TriggerProbability = {defTriggerProbability}"); + m.SendMessage($"ProximityRange = {defProximityRange}"); + m.SendMessage($"ProximityTriggerSound = {defProximityTriggerSound}"); + m.SendMessage($"MinRefractory = {defMinRefractory}"); + m.SendMessage($"MaxRefractory = {defMaxRefractory}"); + m.SendMessage($"TODStart = {defTODStart}"); + m.SendMessage($"TODEnd = {defTODEnd}"); + m.SendMessage($"TODMode = {defTODMode}"); + m.SendMessage($"StackAmount = {defAmount}"); + m.SendMessage($"Duration = {defDuration}"); + m.SendMessage($"Group = {defIsGroup}"); + m.SendMessage($"Team = {defTeam}"); + m.SendMessage($"RelativeHome = {defRelativeHome}"); + m.SendMessage($"SpawnRange = {defSpawnRange}"); + m.SendMessage($"HomeRange = {defHomeRange}"); + m.SendMessage($"MinDelay = {defMinDelay}"); + m.SendMessage($"MaxDelay = {defMaxDelay}"); + } + } + + [Usage("XmlSpawnerShowAll")] + [Aliases("XmlShow")] + [Description("Makes all XmlSpawner objects movable and also changes the item id to a blue ships mast for easy identification.")] + public static void ShowSpawnPoints_OnCommand(CommandEventArgs e) + { + var ToShow = new List(); + foreach (var item in World.Items.Values) + { + if (item is XmlSpawner xmlItem) + { + //turned off visibility. Admins will still see masts but players will not. + xmlItem.Visible = false; // set the spawn item visibility + xmlItem.Movable = false; // Make the spawn item movable + xmlItem.Hue = 88; // Bright blue colour so its easy to spot + xmlItem.ItemID = ShowItemId; // Ship Mast (Very tall, easy to see if beneath other objects) + + // find container-held spawners to be marked with an external static + if (xmlItem.Parent != null && xmlItem.RootParent is Container) + { + ToShow.Add(xmlItem); + } + } + } + + // place the statics + foreach (var xml_item in ToShow) + { + // does the spawner already have a static attached to it? could happen if two showall commands are issued in a row. + // if so then dont add another + if ((xml_item.m_ShowContainerStatic == null || xml_item.m_ShowContainerStatic.Deleted) && xml_item.RootParent is Container rootItem) + { + // calculate a world location for the static. Position it just above the container + var x = rootItem.Location.X; + var y = rootItem.Location.Y; + var z = rootItem.Location.Z + 10; + + var s = new Static(ShowItemId) + { + Visible = false + }; + s.MoveToWorld(new Point3D(x, y, z), rootItem.Map); + + xml_item.m_ShowContainerStatic = s; + } + + } + } + + [Usage("XmlSpawnerHideAll")] + [Aliases("XmlHide")] + [Description("Makes all XmlSpawner objects invisible and unmovable returns the object id to the default.")] + public static void HideSpawnPoints_OnCommand(CommandEventArgs e) + { + var ToDelete = new List(); + foreach (var item in World.Items.Values) + { + if (item is XmlSpawner xmlItem) + { + xmlItem.Visible = false; + xmlItem.Movable = false; + xmlItem.Hue = 0; + xmlItem.ItemID = BaseItemId; + + // get rid of the external static marker for container-held spawners + // check anything that might have been tagged with a container static + if (xmlItem.m_ShowContainerStatic?.Deleted == false) + { + ToDelete.Add(xmlItem); + } + } + } + foreach (var xml_item in ToDelete) + { + if (xml_item.m_ShowContainerStatic?.Deleted == false) + { + xml_item.m_ShowContainerStatic.Delete(); + } + } + } + + [Usage("XmlGo | [z]")] + [Description("Go command used with spawn editor, takes the name of the map as the first parameter.")] + private static void SpawnEditorGo_OnCommand(CommandEventArgs e) + { + if (e == null) + { + return; + } + + var from = e.Mobile; + + // Make sure a map name was given at least + if (from != null && e.Length >= 1) + { + var MapName = e.Arguments[0]; + + // Get the map + Map NewMap; + // Convert the xml map value to a real map object + if (MapName.InsensitiveEquals(Map.Trammel.Name)) + { + NewMap = Map.Trammel; + } + else if (MapName.InsensitiveEquals(Map.Felucca.Name)) + { + NewMap = Map.Felucca; + } + else if (MapName.InsensitiveEquals(Map.Ilshenar.Name)) + { + NewMap = Map.Ilshenar; + } + else if (MapName.InsensitiveEquals(Map.Malas.Name)) + { + NewMap = Map.Malas; + } + else if (MapName.InsensitiveEquals(Map.Tokuno.Name)) + { + NewMap = Map.Tokuno; + } + else + { + from.SendMessage($"Map '{MapName}' does not exist!"); + return; + } + + // Now that the map has been determined, continue + // Check if the request is to simply change maps + if (e.Length == 1) + { + // Map Change ONLY + from.Map = NewMap; + } + else if (e.Length == 3) + { + // Map & X Y ONLY + if (NewMap != null) + { + var x = e.GetInt32(1); + var y = e.GetInt32(2); + var z = NewMap.GetAverageZ(x, y); + from.Map = NewMap; + from.Location = new Point3D(x, y, z); + } + } + else if (e.Length == 4) + { + // Map & X Y Z + from.Map = NewMap; + from.Location = new Point3D(e.GetInt32(1), e.GetInt32(2), e.GetInt32(3)); + } + else + { + from.SendMessage("Format: XmlGo | [z]"); + } + } + } + + [Usage("SmartStat [accesslevel Player/Counselor/GameMaster/Seer/Administrator]")] + [Description("Returns the spawn reduction due to SmartSpawning.")] + public static void SmartStat_OnCommand(CommandEventArgs e) + { + if (e?.Mobile == null) + { + return; + } + + if (e.Arguments.Length > 1 && e.Arguments[0].ToLower() == "accesslevel" && e.Mobile.AccessLevel >= AccessLevel.Administrator) + { + try + { + SmartSpawnAccessLevel = (AccessLevel)Enum.Parse(typeof(AccessLevel), e.Arguments[1], true); + } + catch (Exception ex) { Diagnostics.ExceptionLogging.LogException(ex); } + } + // handle the + // number of spawners + var count = 0; + // number of actual spawns + var currentcount = 0; + var smartcount = 0; + var inactivecount = 0; + // maximum possible spawns + var totalcount = 0; + var maxcount = 0; + // maximum possible of spawns that are currently inactivated + var savings = 0; + foreach (var item in World.Items.Values) + { + if (item is XmlSpawner spawner) + { + if (spawner.Deleted) + { + continue; + } + + totalcount += spawner.MaxCount; + // get the current count without defragging + currentcount += spawner.SafeCurrentCount; + count++; + + // check to see if smartspawning is set + if (spawner.SmartSpawning) + { + smartcount++; + maxcount += spawner.MaxCount; + } + + if (spawner.IsInactivated) + { + inactivecount++; + savings += spawner.MaxCount; + } + } + } + + var percent = 0; + + var maxpercent = 0; + if (totalcount > 0) + { + percent = 100 * savings / totalcount; + maxpercent = 100 * maxcount / totalcount; + } + + var notice = new Gumps.NoticeGump + ( + 1060637, + 30720, + $"Smartspawning access level is {SmartSpawnAccessLevel}\n" + + $"--------------------------------\n" + + $"{count:N0} XmlSpawners\n" + + $"{smartcount:N0} are configured for SmartSpawning\n" + + $"{inactivecount:N0} are currently inactivated\n" + + $"{totalSectorsMonitored:N0} sectors being monitored\n" + + $"Maximum possible spawn count is {totalcount:N0}\n" + + $"Maximum possible spawn reduction is {maxcount:N0}\n" + + $"Current spawn count is {currentcount:N0}\n" + + $"Current spawn reduction is {savings:N0}\n" + + $"Maximum possible savings is {maxpercent}%\n" + + $"Current savings is {percent}%\n", + 0xFFC000, + 420, + 280 + ); + + e.Mobile.SendGump(notice); + } + + [Usage("OptimalSmartSpawning [max spawn/homerange diff]")] + [Description("Activates SmartSpawning on XmlSpawners that are well-suited for use of this feature.")] + public static void OptimalSmartSpawning_OnCommand(CommandEventArgs e) + { + var maxdiff = 1; + if (e.Arguments.Length > 0) + { + try + { + maxdiff = int.Parse(e.Arguments[0]); + } + catch (Exception ex) + { + Diagnostics.ExceptionLogging.LogException(ex); + } + } + var count = 0; + var maxcount = 0; + foreach (var item in World.Items.Values) + { + if (item is XmlSpawner spawner) + { + // determine whether this spawner is a good candidate + + if (spawner.Deleted) + { + continue; + } + + // ignore spawners in towns + //if (Region.Find(spawner.Location, spawner.Map) is Regions.TownRegion) continue; + + // dont bother setting it on triggered spawners + if (spawner.ProximityRange >= 0) + { + continue; + } + + // check the relative spawnrange and homerange. Dont set it on spawners with a larger homerange than spawnrange + var width = spawner.m_Width; + var height = spawner.m_Height; + + if (spawner.HomeRange * 2 > width + maxdiff * 2 || spawner.HomeRange * 2 > height + maxdiff * 2 && spawner.m_Region != null) + { + continue; + } + + var nso = 0; + + if (spawner.m_SpawnObjects != null) + { + nso = spawner.m_SpawnObjects.Count; + } + + // empty spawner so skip it + if (nso == 0) + { + continue; + } + + var skipit = false; + + // check the spawn types + for (var i = 0; i < nso; ++i) + { + var so = spawner.m_SpawnObjects[i]; + + if (so == null) + { + continue; + } + + var typestr = so.TypeName; + + var type = AssemblyHandler.FindTypeByName(typestr); + + // if it has basevendors on it or invalid types, then skip it + if (typestr == null || type != null && (type == typeof(BaseVendor) || type.IsSubclassOf(typeof(BaseVendor))) || + type == null && !BaseXmlSpawner.IsTypeOrItemKeyword(typestr) && !typestr.Contains('{') && !typestr.StartsWith("*") && !typestr.StartsWith("#")) + { + skipit = true; + break; + } + } + + if (!skipit) + { + count++; + spawner.SmartSpawning = true; + maxcount += spawner.MaxCount; + } + } + } + + e.Mobile.SendMessage($"Configured {count:N0} XmlSpawners for SmartSpawning using maxdiff of {maxdiff:N0}"); + e.Mobile.SendMessage($"Estimated item/mob reduction is {maxcount:N0}"); + } + + [Usage("XmlSpawnerWipe [SpawnerPrefixFilter]")] + [Description("Removes all XmlSpawner objects from the current map.")] + public static void Wipe_OnCommand(CommandEventArgs e) + { + WipeSpawners(e, false); + } + + [Usage("XmlSpawnerWipeAll [SpawnerPrefixFilter]")] + [Description("Removes all XmlSpawner objects from the entire world.")] + public static void WipeAll_OnCommand(CommandEventArgs e) + { + WipeSpawners(e, true); + } + + public static void XmlUnLoadFromFile(string filename, string SpawnerPrefix, Mobile from, out int processedmaps, out int processedspawners) + { + + processedmaps = 0; + processedspawners = 0; + if (filename == null || filename.Length <= 0) + { + return; + } + + var total_processed_maps = 0; + var total_processed_spawners = 0; + + // Check if the file exists + if (File.Exists(filename)) + { + FileStream fs = null; + try + { + fs = File.Open(filename, FileMode.Open, FileAccess.Read); + } + catch { } + + if (fs == null) + { + from?.SendMessage($"Unable to open {filename} for unloading"); + + return; + } + + XmlUnLoadFromStream(fs, filename, SpawnerPrefix, from, out processedmaps, out processedspawners); + + } + else if (Directory.Exists(filename)) // check to see if it is a directory + { + // if so then import all of the .xml files in the directory + string[] files = null; + try + { + files = Directory.GetFiles(filename, "*.xml"); + } + catch { } + if (files?.Length > 0) + { + from?.SendMessage($"UnLoading {files.Length} .xml files from directory {filename}"); + + foreach (var file in files) + { + XmlUnLoadFromFile(file, SpawnerPrefix, from, out processedmaps, out processedspawners); + total_processed_maps += processedmaps; + total_processed_spawners += processedspawners; + } + } + // recursively search subdirectories for more .xml files + string[] dirs = null; + try + { + dirs = Directory.GetDirectories(filename); + } + catch { } + if (dirs?.Length > 0) + { + foreach (var dir in dirs) + { + XmlUnLoadFromFile(dir, SpawnerPrefix, from, out processedmaps, out processedspawners); + total_processed_maps += processedmaps; + total_processed_spawners += processedspawners; + } + } + from?.SendMessage($"UnLoaded a total of {total_processed_maps} .xml files and {total_processed_spawners} spawners from directory {filename}"); + + processedmaps = total_processed_maps; + processedspawners = total_processed_spawners; + } + else + { + from?.SendMessage($"{filename} does not exist"); + } + + } + + public static void XmlUnLoadFromStream(Stream fs, string filename, string SpawnerPrefix, Mobile from, out int processedmaps, out int processedspawners) + { + processedmaps = 0; + processedspawners = 0; + + if (fs == null) + { + return; + } + + var TotalCount = 0; + var TrammelCount = 0; + var FeluccaCount = 0; + var IlshenarCount = 0; + var MalasCount = 0; + var TokunoCount = 0; + var OtherCount = 0; + var bad_spawner_count = 0; + var spawners_deleted = 0; + + from?.SendMessage( + $"UnLoading {"XmlSpawner"} objects{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)} from file {filename}." + ); + + // Create the data set + var ds = new DataSet(SpawnDataSetName); + + // Read in the file + //ds.ReadXml(e.Arguments[0].ToString()); + var fileerror = false; + try + { + _ = ds.ReadXml(fs); + } + catch + { + from?.SendMessage(33, $"Error reading xml file {filename}"); + + fileerror = true; + } + // close the file + fs.Close(); + if (fileerror) + { + return; + } + + // Check that at least a single table was loaded + if (ds.Tables.Count > 0) + { + // Add each spawn point to the current map + if (ds.Tables[SpawnTablePointName]?.Rows.Count > 0) + { + foreach (DataRow dr in ds.Tables[SpawnTablePointName].Rows) + { + // load in the spawner info. Certain fields are required and therefore cannot be ignored + // the exception handler for those will flag bad_spawner and the result will be logged + + // Each row makes up a single spawner + var SpawnName = "Spawner"; + try { SpawnName = (string)dr["Name"]; } + catch { } + + // Check if there is any spawner name criteria specified on the unload + if (SpawnerPrefix == null || SpawnerPrefix.Length == 0 || SpawnName.StartsWith(SpawnerPrefix)) + { + var bad_spawner = false; + // Try load the GUID (might not work so create a new GUID) + var SpawnId = Guid.NewGuid(); + try { SpawnId = new Guid((string)dr["UniqueId"]); } + catch { bad_spawner = true; } + // have to have a GUID or no point in continuing + if (bad_spawner) + { + bad_spawner_count++; + continue; + } + // Get the map (default to the mobiles map) + var SpawnMap = Map.Internal; + var XmlMapName = SpawnMap.Name; + + // Try to get the "map" field, but in case it doesn't exist, catch and discard the exception + try { XmlMapName = (string)dr["Map"]; } + catch { } + + // Convert the xml map value to a real map object + if (XmlMapName.InsensitiveEquals(Map.Trammel.Name) || XmlMapName == "Trammel") + { + SpawnMap = Map.Trammel; + TrammelCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Felucca.Name) || XmlMapName == "Felucca") + { + SpawnMap = Map.Felucca; + FeluccaCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Ilshenar.Name) || XmlMapName == "Ilshenar") + { + SpawnMap = Map.Ilshenar; + IlshenarCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Malas.Name) || XmlMapName == "Malas") + { + SpawnMap = Map.Malas; + MalasCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Tokuno.Name) || XmlMapName == "Tokuno") + { + SpawnMap = Map.Tokuno; + TokunoCount++; + } + else + { + try + { + SpawnMap = Map.Parse(XmlMapName); + } + catch { } + OtherCount++; + } + + // Check if this spawner already exists + foreach (var i in World.Items.Values) + { + if (i is XmlSpawner checkXmlSpawner) + { + // Check if the spawners GUID is the same as the one being unloaded + // and that the spawners map is the same as the one being unloaded + if (checkXmlSpawner.UniqueId == SpawnId.ToString() + /*&& (CheckXmlSpawner.Map == SpawnMap)*/) + { + if (checkXmlSpawner != null) + { + spawners_deleted++; + checkXmlSpawner.Delete(); + } + + break; + } + } + } + } + + TotalCount++; + } + } + } + + try + { + fs.Close(); + } + catch { } + + from?.SendMessage( + $"{spawners_deleted}/{TotalCount} spawner(s) were unloaded using file {filename} [Trammel={TrammelCount}, Felucca={FeluccaCount}, Ilshenar={IlshenarCount}, Malas={MalasCount}, Tokuno={TokunoCount}, Other={OtherCount}]." + ); + + if (bad_spawner_count > 0) + { + from?.SendMessage(33, $"{bad_spawner_count} bad spawners detected."); + } + + processedmaps = 1; + processedspawners = TotalCount; + } + + [Usage("XmlSpawnerUnLoad [SpawnerPrefixFilter]")] + [Aliases("XmlUnload")] + [Description("UnLoads XmlSpawner objects from the proper map as defined in the file supplied.")] + public static void UnLoad_OnCommand(CommandEventArgs e) + { + if (e.Mobile.AccessLevel >= DiskAccessLevel) + { + if (e.Arguments.Length >= 1) + { + // Spawner unload criteria (if any) + var SpawnerPrefix = string.Empty; + + // Check if there is an argument provided (load criteria) + if (e.Arguments.Length > 1) + { + SpawnerPrefix = e.Arguments[1]; + } + + var filename = LocateFile(e.Arguments[0]); + XmlUnLoadFromFile(filename, SpawnerPrefix, e.Mobile, out _, out _); + } + else + { + e.Mobile.SendMessage($"Usage: {e.Command} "); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + [Usage("XmlImportMap ")] + [Description("Loads spawner definitions from a .map file")] + public static void XmlImportMap_OnCommand(CommandEventArgs e) + { + if (e.Mobile.AccessLevel >= DiskAccessLevel) + { + if (e.Arguments.Length >= 1) + { + var filename = e.Arguments[0]; + + XmlImportMap(filename, e.Mobile, out _, out _); + } + else + { + e.Mobile.SendMessage($"Usage: {e.Command} "); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + public static void XmlImportMap(string filename, Mobile from, out int processedmaps, out int processedspawners) + { + processedmaps = 0; + processedspawners = 0; + var total_processed_maps = 0; + var total_processed_spawners = 0; + if (filename == null || filename.Length <= 0 || from?.Deleted != false) + { + return; + } + + // Check if the file exists + if (File.Exists(filename)) + { + var spawnercount = 0; + var badspawnercount = 0; + var linenumber = 0; + // default is no map override, use the map spec from each spawn line + var overridemap = -1; + double overridemintime = -1; + double overridemaxtime = -1; + var newformat = false; + try + { + // Create an instance of StreamReader to read from a file. + // The using statement also closes the StreamReader. + using var sr = new StreamReader(filename); + string line; + // Read and display lines from the file until the end of + // the file is reached. + while ((line = sr.ReadLine()) != null) + { + // the old format of each .map line is * Dragon:Wyvern 5209 965 -40 2 2 10 50 30 1 + // * typename:typename:... x y z map mindelay maxdelay homerange spawnrange maxcount + // * | typename:typename:... |s1 |s2 |s3 |s4 |s5 | x | y | z | map | mindelay maxdelay homerange spawnrange spawnid maxcount | maxcount1 | maxcount2 | maxcount3 | maxcount4 | maxcount5 + // where s1-5 are additional spawn type entries with their own maxcounts + // the new format of each .map line is * |Dragon:Wyvern| spawns:spawns| | | | | 5209 | 965 | -40 | 2 | 2 | 10 | 50 | 30 | 1 + + linenumber++; + // is this the new format? + string[] args; + if (line.Contains('|')) + { + args = line.Trim().Split('|'); + newformat = true; + } + else + { + args = line.Trim().Split(' '); + } + + // determine the format of this line and parse accordingly + if (newformat) + { + ParseNewMapFormat(from, filename, line, args, linenumber, ref spawnercount, ref badspawnercount, ref overridemap, ref overridemintime, ref overridemaxtime); + } + else + { + ParseOldMapFormat(from, filename, line, args, linenumber, ref spawnercount, ref badspawnercount, ref overridemap, ref overridemintime, ref overridemaxtime); + } + + } + sr.Close(); + } + catch (Exception e) + { + // Let the user know what went wrong. + from.SendMessage($"The file could not be read: {e.Message}"); + } + from.SendMessage($"Imported {spawnercount} spawners from {filename}"); + from.SendMessage($"{badspawnercount} bad spawners detected"); + processedmaps = 1; + processedspawners = spawnercount; + } + else if (Directory.Exists(filename)) // check to see if it is a directory + { + // if so then import all of the .map files in the directory + string[] files = null; + try + { + files = Directory.GetFiles(filename, "*.map"); + } + catch { } + if (files?.Length > 0) + { + from.SendMessage($"Importing {files.Length} .map files from directory {filename}"); + foreach (var file in files) + { + XmlImportMap(file, from, out processedmaps, out processedspawners); + total_processed_maps += processedmaps; + total_processed_spawners += processedspawners; + } + } + // recursively search subdirectories for more .map files + string[] dirs = null; + try + { + dirs = Directory.GetDirectories(filename); + } + catch { } + if (dirs?.Length > 0) + { + foreach (var dir in dirs) + { + XmlImportMap(dir, from, out processedmaps, out processedspawners); + total_processed_maps += processedmaps; + total_processed_spawners += processedspawners; + } + } + from.SendMessage( + $"Imported a total of {total_processed_maps} .map files and {filename} spawners from directory {total_processed_spawners}" + ); + processedmaps = total_processed_maps; + processedspawners = total_processed_spawners; + } + else + { + from.SendMessage($"{filename} does not exist"); + } + } + + private static void ParseNewMapFormat(Mobile from, string filename, string line, string[] args, int linenumber, ref int spawnercount, ref int badspawnercount, ref int overridemap, ref double overridemintime, ref double overridemaxtime) + { + // format of each .map line is * Dragon:Wyvern 5209 965 -40 2 2 10 50 30 1 + // * typename:typename:... x y z map mindelay maxdelay homerange spawnrange maxcount + // or + // * typename:typename:... x y z map mindelay maxdelay homerange spawnrange spawnid maxcount + // ## are comments + // overridemap mapnumber + // map 0 is tram+fel + // map 1 is fel + // map 2 is tram + // map 3 is ilsh + // map 4 is mal + // map 5 is tokuno + // + // * | typename:typename:... | | | | | | x | y | z | map | mindelay maxdelay homerange spawnrange spawnid maxcount1 | maxcount2 | maxcount2 | maxcount3 | maxcount4 | maxcount5 | maxcount6 + // the new format of each .map line is * |Dragon:Wyvern| spawns:spawns| | | | | 5209 | 965 | -40 | 2 | 2 | 10 | 50 | 30 | 1 + + if (args == null || from == null) + { + return; + } + + // look for the override keyword + if (args.Length == 2 && args[0].ToLower() == "overridemap") + { + try + { + overridemap = int.Parse(args[1]); + } + catch { } + } + else + if (args.Length == 2 && args[0].ToLower() == "overridemintime") + { + try + { + overridemintime = double.Parse(args[1]); + } + catch { } + } + else + if (args.Length == 2 && args[0].ToLower() == "overridemaxtime") + { + try + { + overridemaxtime = double.Parse(args[1]); + } + catch { } + } + else + // look for a spawn spec line + if (args.Length > 0 && args[0] == "*") + { + + var badspawn = false; + var x = 0; + var y = 0; + var z = 0; + var map = 0; + double mindelay = 0; + double maxdelay = 0; + var homerange = 0; + var spawnrange = 0; + var typenames = new string[6][]; + + var maxcount = new int[6]; + + // parse the main args + + try + { + // get the list of spawns + for (var k = 0; k < 6; k++) + { + typenames[k] = args[k + 1].Split(':'); + } + + x = int.Parse(args[7]); + y = int.Parse(args[8]); + z = int.Parse(args[9]); + map = int.Parse(args[10]); + mindelay = double.Parse(args[11]); + maxdelay = double.Parse(args[12]); + homerange = int.Parse(args[13]); + spawnrange = int.Parse(args[14]); + var spawnid = int.Parse(args[15]); + + for (var k = 0; k < 6; k++) + { + maxcount[k] = int.Parse(args[k + 16]); + } + } + catch { from.SendMessage($"Parsing error at line {linenumber}"); badspawn = true; } + + // compute the total number of spawns + var totalspawns = 0; + var totalmaxcount = 0; + + for (var k = 0; k < 6; k++) + { + if (typenames[k] == null) + { + continue; + } + + for (var i = 0; i < typenames[k].Length; i++) + { + if (typenames[k][i] == null || typenames[k][i].Length == 0) + { + continue; + } + + totalspawns++; + } + + totalmaxcount += maxcount[k]; + } + + // apply min/maxdelay overrides + if (overridemintime != -1) + { + mindelay = overridemintime; + } + if (overridemaxtime != -1) + { + maxdelay = overridemaxtime; + } + if (mindelay > maxdelay) + { + maxdelay = mindelay; + } + + if (!badspawn && totalspawns > 0) + { + // everything seems ok so go ahead and make the spawner + // check for map override + if (overridemap >= 0) + { + map = overridemap; + } + + var spawnmap = map switch + { + 0 => Map.Felucca, + 1 => Map.Felucca, + 2 => Map.Trammel, + 3 => Map.Ilshenar, + 4 => Map.Malas, + 5 => Map.Tokuno, + _ => Map.Internal + }; + + if (!IsValidMapLocation(x, y, spawnmap)) + { + // invalid so dont spawn it + badspawnercount++; + from.SendMessage($"Invalid map/location at line {linenumber}"); + from.SendMessage($"Bad spawn at line {line}: {line}"); + return; + } + + // allow it to make an xmlspawner instead + // first add all of the creatures on the list + var so = new SpawnObject[totalspawns]; + var count = 0; + var hasvendor = true; + for (var k = 0; k < 6; k++) + { + if (typenames[k] == null) + { + continue; + } + + for (var i = 0; i < typenames[k].Length; i++) + { + if (typenames[k][i] == null || typenames[k][i].Length == 0 || count > totalspawns) + { + continue; + } + + so[count++] = new SpawnObject(typenames[k][i], maxcount[k]); + + // check the type to see if there are vendors on it + var type = AssemblyHandler.FindTypeByName(typenames[k][i]); + + // check for vendor-only spawners which get special spawnrange treatment + if (type != null && type != typeof(BaseVendor) && !type.IsSubclassOf(typeof(BaseVendor))) + { + hasvendor = false; + } + + } + } + + // assign it a unique id + var SpawnId = Guid.NewGuid(); + + // and give it a name based on the spawner count and file + var spawnername = $"{Path.GetFileNameWithoutExtension(filename)}#{spawnercount}"; + + // Create the new xml spawner + var spawner = new XmlSpawner(SpawnId, x, y, 0, 0, spawnername, totalmaxcount, + TimeSpan.FromMinutes(mindelay), TimeSpan.FromMinutes(maxdelay), TimeSpan.FromMinutes(0), -1, defaultTriggerSound, 1, + 0, homerange, false, so, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), + TimeSpan.FromMinutes(0), null, null, null, null, null, + null, null, null, null, 1, null, false, defTODMode, defKillReset, false, -1, null, false, false, false, null, + TimeSpan.FromHours(0), null, false, null) + { + SpawnRange = hasvendor ? 0 : spawnrange, + + PlayerCreated = true + }; + + spawner.MoveToWorld(new Point3D(x, y, z), spawnmap); + if (spawner.Map == Map.Internal) + { + badspawnercount++; + spawner.Delete(); + from.SendMessage($"Invalid map at line {linenumber}"); + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + return; + } + spawnercount++; + // handle the special case of map 0 that also needs to do trammel + if (map == 0) + { + spawnmap = Map.Trammel; + // assign it a unique id + SpawnId = Guid.NewGuid(); + // Create the new xml spawner + spawner = new XmlSpawner(SpawnId, x, y, 0, 0, spawnername, totalmaxcount, + TimeSpan.FromMinutes(mindelay), TimeSpan.FromMinutes(maxdelay), TimeSpan.FromMinutes(0), -1, defaultTriggerSound, 1, + 0, homerange, false, so, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), + TimeSpan.FromMinutes(0), null, null, null, null, null, + null, null, null, null, 1, null, false, defTODMode, defKillReset, false, -1, null, false, false, false, null, + TimeSpan.FromHours(0), null, false, null) + { + SpawnRange = spawnrange, + PlayerCreated = true + }; + + spawner.MoveToWorld(new Point3D(x, y, z), spawnmap); + if (spawner.Map == Map.Internal) + { + badspawnercount++; + spawner.Delete(); + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + return; + } + spawnercount++; + } + } + else + { + badspawnercount++; + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + } + } + } + + private static void ParseOldMapFormat(Mobile from, string filename, string line, string[] args, int linenumber, ref int spawnercount, ref int badspawnercount, ref int overridemap, ref double overridemintime, ref double overridemaxtime) + { + // format of each .map line is * Dragon:Wyvern 5209 965 -40 2 2 10 50 30 1 + // * typename:typename:... x y z map mindelay maxdelay homerange spawnrange maxcount + // or + // * typename:typename:... x y z map mindelay maxdelay homerange spawnrange spawnid maxcount + // ## are comments + // overridemap mapnumber + // map 0 is tram+fel + // map 1 is fel + // map 2 is tram + // map 3 is ilsh + // map 4 is mal + // map 5 is tokuno + // + // * | typename:typename:... | | | | | | x | y | z | map | mindelay maxdelay homerange spawnrange spawnid maxcount | maxcount2 | maxcount2 | maxcount3 | maxcount4 | maxcount5 + // the new format of each .map line is * |Dragon:Wyvern| spawns:spawns| | | | | 5209 | 965 | -40 | 2 | 2 | 10 | 50 | 30 | 1 + + if (args == null || from == null) + { + return; + } + + // look for the override keyword + if (args.Length == 2 && args[0].ToLower() == "overridemap") + { + try + { + overridemap = int.Parse(args[1]); + } + catch { } + } + else + if (args.Length == 2 && args[0].ToLower() == "overridemintime") + { + try + { + overridemintime = double.Parse(args[1]); + } + catch { } + } + else + if (args.Length == 2 && args[0].ToLower() == "overridemaxtime") + { + try + { + overridemaxtime = double.Parse(args[1]); + } + catch { } + } + else + // look for a spawn spec line + if (args.Length > 0 && args[0] == "*") + { + var badspawn = false; + var x = 0; + var y = 0; + var z = 0; + var map = 0; + double mindelay = 0; + double maxdelay = 0; + var homerange = 0; + var spawnrange = 0; + var maxcount = 0; + string[] typenames = null; + if (args.Length is not 11 and not 12) + { + badspawn = true; + from.SendMessage($"Invalid arg count {args.Length} at line {linenumber}"); + } + else + { + // get the list of spawns + typenames = args[1].Split(':'); + // parse the rest of the args + + if (args.Length == 11) + { + + try + { + x = int.Parse(args[2]); + y = int.Parse(args[3]); + z = int.Parse(args[4]); + map = int.Parse(args[5]); + mindelay = double.Parse(args[6]); + maxdelay = double.Parse(args[7]); + homerange = int.Parse(args[8]); + spawnrange = int.Parse(args[9]); + maxcount = int.Parse(args[10]); + + } + catch { from.SendMessage($"Parsing error at line {linenumber}"); badspawn = true; } + } + else + if (args.Length == 12) + { + + try + { + x = int.Parse(args[2]); + y = int.Parse(args[3]); + z = int.Parse(args[4]); + map = int.Parse(args[5]); + mindelay = double.Parse(args[6]); + maxdelay = double.Parse(args[7]); + homerange = int.Parse(args[8]); + spawnrange = int.Parse(args[9]); + var spawnid = int.Parse(args[10]); + maxcount = int.Parse(args[11]); + + } + catch { from.SendMessage($"Parsing error at line {linenumber}"); badspawn = true; } + } + } + + // apply mi/maxdelay overrides + if (overridemintime != -1) + { + mindelay = overridemintime; + } + if (overridemaxtime != -1) + { + maxdelay = overridemaxtime; + } + if (mindelay > maxdelay) + { + maxdelay = mindelay; + } + + if (!badspawn && typenames.Length > 0) + { + // everything seems ok so go ahead and make the spawner + // check for map override + if (overridemap >= 0) + { + map = overridemap; + } + + var spawnmap = map switch + { + 0 => Map.Felucca, + 1 => Map.Felucca, + 2 => Map.Trammel, + 3 => Map.Ilshenar, + 4 => Map.Malas, + 5 => Map.Tokuno, + _ => Map.Internal + }; + + if (!IsValidMapLocation(x, y, spawnmap)) + { + // invalid so dont spawn it + badspawnercount++; + from.SendMessage($"Invalid map/location at line {linenumber}"); + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + return; + } + + // allow it to make an xmlspawner instead + // first add all of the creatures on the list + var so = new SpawnObject[typenames.Length]; + + var hasvendor = true; + for (var i = 0; i < typenames.Length; i++) + { + so[i] = new SpawnObject(typenames[i], maxcount); + + // check the type to see if there are vendors on it + var type = AssemblyHandler.FindTypeByName(typenames[i]); + + // check for vendor-only spawners which get special spawnrange treatment + if (type != null && type != typeof(BaseVendor) && !type.IsSubclassOf(typeof(BaseVendor))) + { + hasvendor = false; + } + + } + + // assign it a unique id + var SpawnId = Guid.NewGuid(); + + // and give it a name based on the spawner count and file + var spawnername = $"{Path.GetFileNameWithoutExtension(filename)}#{spawnercount}"; + + // Create the new xml spawner + var spawner = new XmlSpawner(SpawnId, x, y, 0, 0, spawnername, maxcount, + TimeSpan.FromMinutes(mindelay), TimeSpan.FromMinutes(maxdelay), TimeSpan.FromMinutes(0), -1, defaultTriggerSound, 1, + 0, homerange, false, so, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), + TimeSpan.FromMinutes(0), null, null, null, null, null, + null, null, null, null, 1, null, false, defTODMode, defKillReset, false, -1, null, false, false, false, null, + TimeSpan.FromHours(0), null, false, null) + { + SpawnRange = hasvendor ? 0 : spawnrange, + + PlayerCreated = true + }; + + spawner.MoveToWorld(new Point3D(x, y, z), spawnmap); + if (spawner.Map == Map.Internal) + { + badspawnercount++; + spawner.Delete(); + from.SendMessage($"Invalid map at line {linenumber}"); + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + return; + } + spawnercount++; + // handle the special case of map 0 that also needs to do trammel + if (map == 0) + { + spawnmap = Map.Trammel; + // assign it a unique id + SpawnId = Guid.NewGuid(); + // Create the new xml spawner + spawner = new XmlSpawner(SpawnId, x, y, 0, 0, spawnername, maxcount, + TimeSpan.FromMinutes(mindelay), TimeSpan.FromMinutes(maxdelay), TimeSpan.FromMinutes(0), -1, defaultTriggerSound, 1, + 0, homerange, false, so, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), + TimeSpan.FromMinutes(0), null, null, null, null, null, + null, null, null, null, 1, null, false, defTODMode, defKillReset, false, -1, null, false, false, false, null, + TimeSpan.FromHours(0), null, false, null) + { + SpawnRange = spawnrange, + PlayerCreated = true + }; + + spawner.MoveToWorld(new Point3D(x, y, z), spawnmap); + if (spawner.Map == Map.Internal) + { + badspawnercount++; + spawner.Delete(); + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + return; + } + spawnercount++; + } + } + else + { + badspawnercount++; + from.SendMessage($"Bad spawn at line {linenumber}: {line}"); + } + } + } + + [Usage("XmlImportSpawners filename")] + [Description("Loads xml files created by Sno's xml exporter as xmlspawners.")] + public static void XmlImportSpawners_OnCommand(CommandEventArgs e) + { + if (e.Arguments.Length >= 1) + { + var filename = e.GetString(0); + var filePath = Path.Combine("Saves/Spawners", filename); + if (File.Exists(filePath)) + { + var doc = new XmlDocument(); + try + { + doc.Load(filePath); + } + catch + { + e.Mobile.SendMessage($"unable to load file {filePath}."); + return; + } + + var root = doc["spawners"]; + int successes = 0, failures = 0; + if (root?.GetElementsByTagName("spawner") != null) + { + foreach (XmlElement spawner in root.GetElementsByTagName("spawner")) + { + try + { + ImportSpawner(spawner); + successes++; + } + catch (Exception ex) { e.Mobile.SendMessage(33, $"{ex.Message} {spawner.InnerText}"); failures++; } + } + } + e.Mobile.SendMessage($"{successes:N0} spawners loaded successfully from {filePath}, {failures:N0} failures."); + } + else + { + e.Mobile.SendMessage($"File {filePath} does not exist."); + } + } + else + { + e.Mobile.SendMessage("Usage: [XmlImportSpawners "); + } + } + + private static string GetText(XmlElement node, string defaultValue) + { + if (node == null) + { + return defaultValue; + } + + return node.InnerText; + } + + private static void ImportSpawner(XmlElement node) + { + var count = int.Parse(GetText(node["count"], "1")); + var homeRange = int.Parse(GetText(node["homerange"], "4")); + var walkingRange = int.Parse(GetText(node["walkingrange"], "-1")); + // width of the spawning area + var spawnwidth = homeRange * 2; + if (walkingRange >= 0) + { + spawnwidth = walkingRange * 2; + } + + var team = int.Parse(GetText(node["team"], "0")); + var group = bool.Parse(GetText(node["group"], "False")); + var maxDelay = TimeSpan.Parse(GetText(node["maxdelay"], "10:00")); + var minDelay = TimeSpan.Parse(GetText(node["mindelay"], "05:00")); + var creaturesName = LoadCreaturesName(node["creaturesname"]); + var name = GetText(node["name"], "Spawner"); + var location = Point3D.Parse(GetText(node["location"], "Error")); + var map = Map.Parse(GetText(node["map"], "Error")); + + // allow it to make an xmlspawner instead + // first add all of the creatures on the list + var so = new SpawnObject[creaturesName.Count]; + + var hasvendor = false; + + for (var i = 0; i < creaturesName.Count; i++) + { + so[i] = new SpawnObject(creaturesName[i], count); + // check the type to see if there are vendors on it + var type = AssemblyHandler.FindTypeByName(creaturesName[i]); + + // if it has basevendors on it or invalid types, then skip it + if (type != null && (type == typeof(BaseVendor) || type.IsSubclassOf(typeof(BaseVendor)))) + { + hasvendor = true; + } + } + + // assign it a unique id + var SpawnId = Guid.NewGuid(); + + // Create the new xml spawner + var spawner = new XmlSpawner(SpawnId, location.X, location.Y, spawnwidth, spawnwidth, name, count, + minDelay, maxDelay, TimeSpan.FromMinutes(0), -1, defaultTriggerSound, 1, + team, homeRange, false, so, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(0), + TimeSpan.FromMinutes(0), null, null, null, null, null, + null, null, null, null, 1, null, group, defTODMode, defKillReset, false, -1, null, false, false, false, null, defDespawnTime, null, false, null) + { + SpawnRange = hasvendor ? 0 : homeRange, + PlayerCreated = true + }; + + spawner.MoveToWorld(location, map); + if (!IsValidMapLocation(location, spawner.Map)) + { + spawner.Delete(); + throw new Exception("Invalid spawner location."); + } + } + + private static List LoadCreaturesName(XmlElement node) + { + var names = new List(); + + if (node != null) + { + foreach (XmlElement ele in node.GetElementsByTagName("creaturename")) + { + if (ele != null) + { + names.Add(ele.InnerText); + } + } + } + + return names; + } + + public static void XmlLoadFromFile(string filename, string SpawnerPrefix, Mobile from, Point3D fromloc, Map frommap, bool loadrelative, int maxrange, bool loadnew, out int processedmaps, out int processedspawners) + { + processedmaps = 0; + processedspawners = 0; + var total_processed_maps = 0; + var total_processed_spawners = 0; + + if (filename == null || filename.Length <= 0) + { + return; + } + + // Check if the file exists + if (File.Exists(filename)) + { + FileStream fs = null; + try + { + fs = File.Open(filename, FileMode.Open, FileAccess.Read); + } + catch { } + + if (fs == null) + { + from?.SendMessage($"Unable to open {filename} for loading"); + + return; + } + + // load the file + XmlLoadFromStream(fs, filename, SpawnerPrefix, from, fromloc, frommap, loadrelative, maxrange, loadnew, out processedmaps, out processedspawners); + + } + else if (Directory.Exists(filename)) + { + // if so then load all of the .xml files in the directory + string[] files = null; + try + { + files = Directory.GetFiles(filename, "*.xml"); + } + catch { } + if (files?.Length > 0) + { + from?.SendMessage($"Loading {files.Length} .xml files from directory {filename}"); + + foreach (var file in files) + { + XmlLoadFromFile(file, SpawnerPrefix, from, fromloc, frommap, loadrelative, maxrange, loadnew, out processedmaps, out processedspawners); + total_processed_maps += processedmaps; + total_processed_spawners += processedspawners; + } + } + // recursively search subdirectories for more .xml files + string[] dirs = null; + try + { + dirs = Directory.GetDirectories(filename); + } + catch { } + if (dirs?.Length > 0) + { + foreach (var dir in dirs) + { + XmlLoadFromFile(dir, SpawnerPrefix, from, fromloc, frommap, loadrelative, maxrange, loadnew, out processedmaps, out processedspawners); + total_processed_maps += processedmaps; + total_processed_spawners += processedspawners; + } + } + from?.SendMessage($"Loaded a total of {total_processed_maps} .xml files and {filename} spawners from directory {total_processed_spawners}"); + + processedmaps = total_processed_maps; + processedspawners = total_processed_spawners; + } + else + { + from?.SendMessage($"{filename} does not exist"); + } + + } + + public static void XmlLoadFromFile(string filename, string SpawnerPrefix, Mobile from, bool loadrelative, int maxrange, bool loadnew, out int processedmaps, out int processedspawners) + { + processedmaps = 0; + processedspawners = 0; + + if (from == null) + { + return; + } + + XmlLoadFromFile(filename, SpawnerPrefix, from, from.Location, from.Map, loadrelative, maxrange, loadnew, out processedmaps, out processedspawners); + } + + public static void XmlLoadFromFile(string filename, string SpawnerPrefix, Point3D fromloc, Map frommap, bool loadrelative, int maxrange, bool loadnew, out int processedmaps, out int processedspawners) + { + XmlLoadFromFile(filename, SpawnerPrefix, null, fromloc, frommap, loadrelative, maxrange, loadnew, out processedmaps, out processedspawners); + + } + + public static void XmlLoadFromFile(string filename, string SpawnerPrefix, bool loadnew, out int processedmaps, out int processedspawners) + { + XmlLoadFromFile(filename, SpawnerPrefix, null, Point3D.Zero, Map.Internal, false, 0, loadnew, out processedmaps, out processedspawners); + + } + + public static void XmlLoadFromStream(Stream fs, string filename, string SpawnerPrefix, Mobile from, Point3D fromloc, Map frommap, bool loadrelative, int maxrange, bool loadnew, out int processedmaps, out int processedspawners) + { + XmlLoadFromStream(fs, filename, SpawnerPrefix, from, fromloc, frommap, loadrelative, maxrange, loadnew, out processedmaps, out processedspawners, false); + } + + public static void XmlLoadFromStream(Stream fs, string filename, string SpawnerPrefix, Mobile from, Point3D fromloc, Map frommap, bool loadrelative, int maxrange, bool loadnew, out int processedmaps, out int processedspawners, bool verbose) + { + processedmaps = 0; + processedspawners = 0; + + if (fs == null) + { + return; + } + + // assign an id that will be used to distinguish the newly loaded spawners by appending it to their name + var newloadid = Guid.NewGuid(); + + var TotalCount = 0; + var TrammelCount = 0; + var FeluccaCount = 0; + var IlshenarCount = 0; + var MalasCount = 0; + var TokunoCount = 0; + var OtherCount = 0; + var questionable_spawner = false; + var bad_spawner = false; + var badcount = 0; + var questionablecount = 0; + + var failedobjectitemcount = 0; + var failedsetitemcount = 0; + var relativex = -1; + var relativey = -1; + var relativez = 0; + Map relativemap = null; + + from?.SendMessage($"Loading {"XmlSpawner"} objects{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)} from file {filename}."); + + // Create the data set + var ds = new DataSet(SpawnDataSetName); + + // Read in the file + var fileerror = false; + try + { + _ = ds.ReadXml(fs); + } + catch + { + from?.SendMessage(33, $"Error reading xml file {filename}"); + + fileerror = true; + } + // close the file + fs.Close(); + if (fileerror) + { + return; + } + + // Check that at least a single table was loaded + if (ds.Tables.Count > 0) + { + // Add each spawn point to the current map + if (ds.Tables[SpawnTablePointName]?.Rows.Count > 0) + { + foreach (DataRow dr in ds.Tables[SpawnTablePointName].Rows) + { + // load in the spawner info. Certain fields are required and therefore cannot be ignored + // the exception handler for those will flag bad_spawner and the result will be logged + + // Each row makes up a single spawner + var SpawnName = "Spawner"; + try { SpawnName = (string)dr["Name"]; } + catch { questionable_spawner = true; } + + if (loadnew) + { + // append the new id to the name + SpawnName = $"{SpawnName}-{newloadid}"; + } + + // Check if there is any spawner name criteria specified on the load + if (string.IsNullOrEmpty(SpawnerPrefix) || SpawnName.StartsWith(SpawnerPrefix)) + { + // Try load the GUID (might not work so create a new GUID) + var SpawnId = Guid.NewGuid(); + if (!loadnew) + { + try { SpawnId = new Guid((string)dr["UniqueId"]); } + catch { } + } + else + { + // change the dataset guid to the newly created one when new loading + try + { + dr["UniqueId"] = SpawnId; + } + catch { Console.WriteLine("unable to set UniqueId"); } + } + + var SpawnCentreX = fromloc.X; + var SpawnCentreY = fromloc.Y; + var SpawnCentreZ = fromloc.Z; + + try { SpawnCentreX = int.Parse((string)dr["CentreX"]); } + catch { bad_spawner = true; } + try { SpawnCentreY = int.Parse((string)dr["CentreY"]); } + catch { bad_spawner = true; } + try { SpawnCentreZ = int.Parse((string)dr["CentreZ"]); } + catch { bad_spawner = true; } + + var SpawnX = SpawnCentreX; + var SpawnY = SpawnCentreY; + var SpawnWidth = 0; + var SpawnHeight = 0; + try { SpawnX = int.Parse((string)dr["X"]); } + catch { questionable_spawner = true; } + try { SpawnY = int.Parse((string)dr["Y"]); } + catch { questionable_spawner = true; } + try { SpawnWidth = int.Parse((string)dr["Width"]); } + catch { questionable_spawner = true; } + try { SpawnHeight = int.Parse((string)dr["Height"]); } + catch { questionable_spawner = true; } + + // Try load the InContainer (default to false) + var InContainer = false; + var ContainerX = 0; + var ContainerY = 0; + var ContainerZ = 0; + try { InContainer = bool.Parse((string)dr["InContainer"]); } + catch { } + if (InContainer) + { + try { ContainerX = int.Parse((string)dr["ContainerX"]); } + catch { } + try { ContainerY = int.Parse((string)dr["ContainerY"]); } + catch { } + try { ContainerZ = int.Parse((string)dr["ContainerZ"]); } + catch { } + } + + // Get the map (default to the mobiles map) if the relative distance is too great, then use the defined map + + var SpawnMap = frommap; + + var XmlMapName = frommap.Name; + + //if (!loadrelative && !loadnew) + { + // Try to get the "map" field, but in case it doesn't exist, catch and discard the exception + try { XmlMapName = (string)dr["Map"]; } + catch { questionable_spawner = true; } + + // Convert the xml map value to a real map object + if (XmlMapName.InsensitiveEquals(Map.Trammel.Name) || XmlMapName == "Trammel") + { + SpawnMap = Map.Trammel; + TrammelCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Felucca.Name) || XmlMapName == "Felucca") + { + SpawnMap = Map.Felucca; + FeluccaCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Ilshenar.Name) || XmlMapName == "Ilshenar") + { + SpawnMap = Map.Ilshenar; + IlshenarCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Malas.Name) || XmlMapName == "Malas") + { + SpawnMap = Map.Malas; + MalasCount++; + } + else if (XmlMapName.InsensitiveEquals(Map.Tokuno.Name) || XmlMapName == "Tokuno") + { + SpawnMap = Map.Tokuno; + TokunoCount++; + } + else + { + try + { + SpawnMap = Map.Parse(XmlMapName); + } + catch { } + OtherCount++; + } + } + + // test to see whether the distance between the relative center point and the spawner is too great. If so then dont do relative + if (relativex == -1 && relativey == -1) + { + // the first xml entry in the file will determine the origin + relativex = SpawnCentreX; + relativey = SpawnCentreY; + relativez = SpawnCentreZ; + + // and also the relative map to relocate from + relativemap = SpawnMap; + } + + var SpawnRelZ = 0; + var OrigZ = SpawnCentreZ; + if (loadrelative && Math.Abs(relativex - SpawnCentreX) <= maxrange && Math.Abs(relativey - SpawnCentreY) <= maxrange + && SpawnMap == relativemap) + { + // its within range so shift it + SpawnCentreX -= relativex - fromloc.X; + SpawnCentreY -= relativey - fromloc.Y; + SpawnX -= relativex - fromloc.X; + SpawnY -= relativey - fromloc.Y; + // force it to autosearch for Z when it places it but hold onto relative Z info just in case it can be placed there + SpawnRelZ = relativez - fromloc.Z; + SpawnCentreZ = short.MinValue; + } + + // if relative loading has been specified, see if the loaded map is the same as the relativemap and relocate. + // if it doesnt match then just leave it + if (loadrelative && relativemap == SpawnMap) + { + SpawnMap = frommap; + } + + if (SpawnMap == Map.Internal) + { + bad_spawner = true; + } + + // Try load the IsRelativeHomeRange (default to true) + var SpawnIsRelativeHomeRange = true; + try { SpawnIsRelativeHomeRange = bool.Parse((string)dr["IsHomeRangeRelative"]); } + catch { } + + var SpawnHomeRange = 5; + try { SpawnHomeRange = int.Parse((string)dr["Range"]); } + catch { questionable_spawner = true; } + var SpawnMaxCount = 1; + try { SpawnMaxCount = int.Parse((string)dr["MaxCount"]); } + catch { questionable_spawner = true; } + + //deal with double format for delay. default is the old minute format + var delay_in_sec = false; + try { delay_in_sec = bool.Parse((string)dr["DelayInSec"]); } + catch { } + var SpawnMinDelay = TimeSpan.FromMinutes(5); + var SpawnMaxDelay = TimeSpan.FromMinutes(10); + + if (delay_in_sec) + { + try { SpawnMinDelay = TimeSpan.FromSeconds(int.Parse((string)dr["MinDelay"])); } + catch { } + try { SpawnMaxDelay = TimeSpan.FromSeconds(int.Parse((string)dr["MaxDelay"])); } + catch { } + } + else + { + try { SpawnMinDelay = TimeSpan.FromMinutes(int.Parse((string)dr["MinDelay"])); } + catch { } + try { SpawnMaxDelay = TimeSpan.FromMinutes(int.Parse((string)dr["MaxDelay"])); } + catch { } + } + var SpawnMinRefractory = TimeSpan.FromMinutes(0); + try { SpawnMinRefractory = TimeSpan.FromMinutes(double.Parse((string)dr["MinRefractory"])); } + catch { } + + var SpawnMaxRefractory = TimeSpan.FromMinutes(0); + try { SpawnMaxRefractory = TimeSpan.FromMinutes(double.Parse((string)dr["MaxRefractory"])); } + catch { } + + var SpawnTODStart = TimeSpan.FromMinutes(0); + try { SpawnTODStart = TimeSpan.FromMinutes(double.Parse((string)dr["TODStart"])); } + catch { } + + var SpawnTODEnd = TimeSpan.FromMinutes(0); + try { SpawnTODEnd = TimeSpan.FromMinutes(double.Parse((string)dr["TODEnd"])); } + catch { } + + var todmode = (int)TODModeType.Realtime; + var SpawnTODMode = TODModeType.Realtime; + try { todmode = int.Parse((string)dr["TODMode"]); } + catch { } + + SpawnTODMode = todmode switch + { + (int)TODModeType.Gametime => TODModeType.Gametime, + (int)TODModeType.Realtime => TODModeType.Realtime, + _ => SpawnTODMode + }; + + var SpawnKillReset = defKillReset; + try { SpawnKillReset = int.Parse((string)dr["KillReset"]); } + catch { } + + string SpawnProximityMessage = null; + // proximity message + try { SpawnProximityMessage = (string)dr["ProximityTriggerMessage"]; } + catch { } + + string SpawnItemTriggerName = null; + try { SpawnItemTriggerName = (string)dr["ItemTriggerName"]; } + catch { } + + string SpawnNoItemTriggerName = null; + try { SpawnNoItemTriggerName = (string)dr["NoItemTriggerName"]; } + catch { } + + string SpawnSpeechTrigger = null; + try { SpawnSpeechTrigger = (string)dr["SpeechTrigger"]; } + catch { } + + string SpawnSkillTrigger = null; + try { SpawnSkillTrigger = (string)dr["SkillTrigger"]; } + catch { } + + string SpawnMobTriggerName = null; + try { SpawnMobTriggerName = (string)dr["MobTriggerName"]; } + catch { } + string SpawnMobPropertyName = null; + try { SpawnMobPropertyName = (string)dr["MobPropertyName"]; } + catch { } + string SpawnPlayerPropertyName = null; + try { SpawnPlayerPropertyName = (string)dr["PlayerPropertyName"]; } + catch { } + + double SpawnTriggerProbability = 1; + try { SpawnTriggerProbability = double.Parse((string)dr["TriggerProbability"]); } + catch { } + + var SpawnSequentialSpawning = -1; + try { SpawnSequentialSpawning = int.Parse((string)dr["SequentialSpawning"]); } + catch { } + + string SpawnRegionName = null; + try { SpawnRegionName = (string)dr["RegionName"]; } + catch { } + + string SpawnConfigFile = null; + try { SpawnConfigFile = (string)dr["ConfigFile"]; } + catch { } + + var SpawnAllowGhost = false; + try { SpawnAllowGhost = bool.Parse((string)dr["AllowGhostTriggering"]); } + catch { } + + var SpawnAllowNPC = false; + try { SpawnAllowNPC = bool.Parse((string)dr["AllowNPCTriggering"]); } + catch { } + + var SpawnSpawnOnTrigger = false; + try { SpawnSpawnOnTrigger = bool.Parse((string)dr["SpawnOnTrigger"]); } + catch { } + + var SpawnSmartSpawning = false; + try { SpawnSmartSpawning = bool.Parse((string)dr["SmartSpawning"]); } + catch { } + + var TickReset = false; + try { TickReset = bool.Parse((string)dr["TickReset"]); } + catch { } + + string SpawnObjectPropertyName = null; + try { SpawnObjectPropertyName = (string)dr["ObjectPropertyName"]; } + catch { } + + // we will assign this during the self-reference resolution pass + Item SpawnSetPropertyItem = null; + + // we will assign this during the self-reference resolution pass + Item SpawnObjectPropertyItem = null; + + // read the duration parameter from the xml file + // but older files wont have it so deal with that condition and set it to the default of "0", i.e. infinite duration + // Try to get the "Duration" field, but in case it doesn't exist, catch and discard the exception + var SpawnDuration = TimeSpan.FromMinutes(0); + try { SpawnDuration = TimeSpan.FromMinutes(double.Parse((string)dr["Duration"])); } + catch { } + + var SpawnDespawnTime = TimeSpan.FromHours(0); + try { SpawnDespawnTime = TimeSpan.FromHours(double.Parse((string)dr["DespawnTime"])); } + catch { } + var SpawnProximityRange = -1; + // Try to get the "ProximityRange" field, but in case it doesn't exist, catch and discard the exception + try { SpawnProximityRange = int.Parse((string)dr["ProximityRange"]); } + catch { } + + var SpawnProximityTriggerSound = 0; + // Try to get the "ProximityTriggerSound" field, but in case it doesn't exist, catch and discard the exception + try { SpawnProximityTriggerSound = int.Parse((string)dr["ProximityTriggerSound"]); } + catch { } + + var SpawnAmount = 1; + try { SpawnAmount = int.Parse((string)dr["Amount"]); } + catch { } + + var SpawnExternalTriggering = false; + try { SpawnExternalTriggering = bool.Parse((string)dr["ExternalTriggering"]); } + catch { } + + string waypointstr = null; + try { waypointstr = (string)dr["Waypoint"]; } + catch { } + + var SpawnWaypoint = GetWaypoint(waypointstr); + + var SpawnTeam = 0; + try { SpawnTeam = int.Parse((string)dr["Team"]); } + catch { questionable_spawner = true; } + var SpawnIsGroup = false; + try { SpawnIsGroup = bool.Parse((string)dr["IsGroup"]); } + catch { questionable_spawner = true; } + var SpawnIsRunning = false; + try { SpawnIsRunning = bool.Parse((string)dr["IsRunning"]); } + catch { questionable_spawner = true; } + // try loading the new spawn specifications first + var Spawns = Array.Empty(); + var havenew = true; + try { Spawns = SpawnObject.LoadSpawnObjectsFromString2((string)dr["Objects2"]); } + catch { havenew = false; } + if (!havenew) + { + // try loading the new spawn specifications + try { Spawns = SpawnObject.LoadSpawnObjectsFromString((string)dr["Objects"]); } + catch { questionable_spawner = true; } + // can only have one of these defined + } + + // do a check on the location of the spawner + if (!IsValidMapLocation(SpawnCentreX, SpawnCentreY, SpawnMap)) + { + from?.SendMessage(33, $"Invalid location '{SpawnName}' at [{SpawnCentreX} {SpawnCentreY}] in {XmlMapName}"); + + bad_spawner = true; + } + + // Check if this spawner already exists + XmlSpawner OldSpawner = null; + var found_container = false; + var found_spawner = false; + Container spawn_container = null; + if (!bad_spawner) + { + foreach (var i in World.Items.Values) + { + if (i is XmlSpawner checkXmlSpawner) + { + // Check if the spawners GUID is the same as the one being loaded + // and that the spawners map is the same as the one being loaded + if (checkXmlSpawner.UniqueId == SpawnId.ToString() + /* && (CheckXmlSpawner.Map == SpawnMap || loadrelative)*/) + { + OldSpawner = checkXmlSpawner; + found_spawner = true; + } + } + + //look for containers with the spawn coordinates if the incontainer flag is set + if (InContainer && !found_container && i is Container container && SpawnCentreX == container.Location.X && SpawnCentreY == container.Location.Y && + (SpawnCentreZ == container.Location.Z || SpawnCentreZ == short.MinValue)) + { + // assume this is the container that the spawner was in + found_container = true; + spawn_container = container; + } + // ok we can break if we have handled both the spawner and any containers + if (found_spawner && (found_container || !InContainer)) + { + break; + } + } + } + + // test to see whether the spawner specification was valid, bad, or questionable + if (bad_spawner) + { + badcount++; + from?.SendMessage(33, "Invalid spawner"); + + // log it + long fileposition = -1; + try { fileposition = fs.Position; } + catch { } + try + { + using var op = new StreamWriter("badxml.log", true); + op.WriteLine("# Invalid spawner : {0}: Fileposition {1} {2}", Core.Now, fileposition, filename); + op.WriteLine(); + } + catch { } + } + else + if (questionable_spawner) + { + questionablecount++; + from?.SendMessage(33, $"Questionable spawner '{SpawnName}' at [{SpawnCentreX} {SpawnCentreY}] in {XmlMapName}"); + + // log it + long fileposition = -1; + try { fileposition = fs.Position; } + catch { } + try + { + using var op = new StreamWriter("badxml.log", true); + op.WriteLine("# Questionable spawner : {0}: Format: X Y Z Map SpawnerName Fileposition Xmlfile", Core.Now); + op.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}", SpawnCentreX, SpawnCentreY, SpawnCentreZ, XmlMapName, SpawnName, fileposition, filename); + op.WriteLine(); + } + catch { } + } + if (!bad_spawner) + { + // Delete the old spawner if it exists + OldSpawner?.Delete(); + + // Create the new spawner + var TheSpawn = new XmlSpawner(SpawnId, SpawnX, SpawnY, SpawnWidth, SpawnHeight, SpawnName, SpawnMaxCount, + SpawnMinDelay, SpawnMaxDelay, SpawnDuration, SpawnProximityRange, SpawnProximityTriggerSound, SpawnAmount, + SpawnTeam, SpawnHomeRange, SpawnIsRelativeHomeRange, Spawns, SpawnMinRefractory, SpawnMaxRefractory, SpawnTODStart, + SpawnTODEnd, SpawnObjectPropertyItem, SpawnObjectPropertyName, SpawnProximityMessage, SpawnItemTriggerName, SpawnNoItemTriggerName, + SpawnSpeechTrigger, SpawnMobTriggerName, SpawnMobPropertyName, SpawnPlayerPropertyName, SpawnTriggerProbability, + SpawnSetPropertyItem, SpawnIsGroup, SpawnTODMode, SpawnKillReset, SpawnExternalTriggering, SpawnSequentialSpawning, + SpawnRegionName, SpawnAllowGhost, SpawnAllowNPC, SpawnSpawnOnTrigger, SpawnConfigFile, SpawnDespawnTime, SpawnSkillTrigger, SpawnSmartSpawning, SpawnWaypoint) + { + DisableGlobalAutoReset = TickReset + }; + + // Try to find a valid Z height if required (SpawnCentreZ = short.MinValue) + var NewZ = 0; + + // Check if relative loading is set. If so then try loading at the z-offset position first with no surface requirement, then try auto + /*if (loadrelative && SpawnMap.CanFit(SpawnCentreX, SpawnCentreY, OrigZ - SpawnRelZ, SpawnFitSize,true, false,false)) */ + + if (loadrelative && HasTileSurface(SpawnMap, SpawnCentreX, SpawnCentreY, OrigZ - SpawnRelZ)) + { + NewZ = OrigZ - SpawnRelZ; + } + else if (SpawnCentreZ == short.MinValue) + { + NewZ = SpawnMap.GetAverageZ(SpawnCentreX, SpawnCentreY); + + if (SpawnMap.CanFit(SpawnCentreX, SpawnCentreY, NewZ, SpawnFitSize) == false) + { + for (var x = 1; x <= 39; x++) + { + if (SpawnMap.CanFit(SpawnCentreX, SpawnCentreY, NewZ + x, SpawnFitSize)) + { + NewZ += x; + break; + } + } + } + } + else + { + // This spawn point already has a defined Z location, so use it + NewZ = SpawnCentreZ; + } + + // if this is a container held spawner, drop it in the container + if (found_container && spawn_container?.Deleted == false) + { + TheSpawn.Location = new Point3D(ContainerX, ContainerY, ContainerZ); + spawn_container.AddItem(TheSpawn); + } + else + { + // disable the X_Y adjustments in OnLocationChange + TheSpawn.IgnoreLocationChange = true; + TheSpawn.MoveToWorld(new Point3D(SpawnCentreX, SpawnCentreY, NewZ), SpawnMap); + } + + // reset the spawner + TheSpawn.Reset(); + TheSpawn.Running = SpawnIsRunning; + + // update subgroup-specific next spawn times + TheSpawn.NextSpawn = TimeSpan.Zero; + TheSpawn.ResetNextSpawnTimes(); + + // Send a message to the client that the spawner is created + if (from != null && verbose) + { + from.SendMessage(188, $"Created '{TheSpawn.Name}' in {TheSpawn.Map.Name} at {TheSpawn.Location}"); + } + + // Do a total respawn + //TheSpawn.Respawn(); + + // Increment the count + TotalCount++; + } + bad_spawner = false; + questionable_spawner = false; + } + } + } + + from?.SendMessage("Resolving spawner self references"); + + if (ds.Tables[SpawnTablePointName]?.Rows.Count > 0) + { + foreach (DataRow dr in ds.Tables[SpawnTablePointName].Rows) + { + // Try load the GUID + var badid = false; + var SpawnId = Guid.NewGuid(); + try { SpawnId = new Guid((string)dr["UniqueId"]); } + catch { badid = true; } + if (badid) + { + continue; + } + + // Get the map + var SpawnMap = frommap; + var XmlMapName = frommap.Name; + + if (!loadrelative) + { + try { XmlMapName = (string)dr["Map"]; } + catch { } + + // Convert the xml map value to a real map object + try + { + SpawnMap = Map.Parse(XmlMapName); + } + catch { } + } + + var found_spawner = false; + XmlSpawner OldSpawner = null; + foreach (var i in World.Items.Values) + { + if (i is XmlSpawner checkXmlSpawner) + { + // Check if the spawners GUID is the same as the one being loaded + // and that the spawners map is the same as the one being loaded + if (checkXmlSpawner.UniqueId == SpawnId.ToString() + /* && (CheckXmlSpawner.Map == SpawnMap || loadrelative) */) + { + OldSpawner = checkXmlSpawner; + found_spawner = true; + } + } + + if (found_spawner) + { + break; + } + } + + if (found_spawner && OldSpawner?.Deleted == false) + { + // resolve item name references since they may have referred to spawners that were just created + string setObjectName = null; + try { setObjectName = (string)dr["SetPropertyItemName"]; } + catch { } + if (!string.IsNullOrEmpty(setObjectName)) + { + // try to parse out the type information if it has also been saved + var typeargs = setObjectName.Split(",".ToCharArray(), 2); + string typestr = null; + var namestr = setObjectName; + + if (typeargs.Length > 1) + { + namestr = typeargs[0]; + typestr = typeargs[1]; + } + + // if this is a new load then assume that it will be referring to another newly loaded object so append the newloadid + if (loadnew) + { + var tmpsetObjectName = $"{namestr}-{newloadid}"; + OldSpawner.SetItem = BaseXmlSpawner.FindItemByName(null, tmpsetObjectName, typestr); + } + // if this fails then try the original + OldSpawner.SetItem ??= BaseXmlSpawner.FindItemByName(null, namestr, typestr); + if (OldSpawner.SetItem == null) + { + failedsetitemcount++; + from?.SendMessage(33, $"Failed to initialize SetItemProperty Object '{setObjectName}' on ' '{OldSpawner.Name}' at [{OldSpawner.Location.X} {OldSpawner.Location.Y}] in {OldSpawner.Map}"); + + // log it + try + { + using var op = new StreamWriter("badxml.log", true); + op.WriteLine("# Failed SetItemProperty Object initialization : {0}: Format: ObjectName X Y Z Map SpawnerName Xmlfile", + Core.Now); + op.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}", + setObjectName, OldSpawner.Location.X, OldSpawner.Location.Y, OldSpawner.Location.Z, OldSpawner.Map, OldSpawner.Name, filename); + op.WriteLine(); + } + catch { } + } + } + + string triggerObjectName = null; + try { triggerObjectName = (string)dr["ObjectPropertyItemName"]; } + catch { } + + if (!string.IsNullOrEmpty(triggerObjectName)) + { + var typeargs = triggerObjectName.Split(",".ToCharArray(), 2); + string typestr = null; + var namestr = triggerObjectName; + + if (typeargs.Length > 1) + { + namestr = typeargs[0]; + typestr = typeargs[1]; + } + + // if this is a new load then assume that it will be referring to another newly loaded object so append the newloadid + if (loadnew) + { + var tmptriggerObjectName = $"{namestr}-{newloadid}"; + OldSpawner.m_ObjectPropertyItem = BaseXmlSpawner.FindItemByName(null, tmptriggerObjectName, typestr); + } + // if this fails then try the original + OldSpawner.m_ObjectPropertyItem ??= BaseXmlSpawner.FindItemByName(null, namestr, typestr); + if (OldSpawner.m_ObjectPropertyItem == null) + { + failedobjectitemcount++; + from?.SendMessage(33, $"Failed to initialize TriggerObject '{triggerObjectName}' on ' '{OldSpawner.Name}' at [{OldSpawner.Location.X} {OldSpawner.Location.Y}] in {OldSpawner.Map}"); + + // log it + try + { + using var op = new StreamWriter("badxml.log", true); + op.WriteLine("# Failed TriggerObject initialization : {0}: Format: ObjectName X Y Z Map SpawnerName Xmlfile", + Core.Now); + op.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}", + triggerObjectName, OldSpawner.Location.X, OldSpawner.Location.Y, OldSpawner.Location.Z, OldSpawner.Map, OldSpawner.Name, filename); + op.WriteLine(); + } + catch { } + } + } + } + } + } + } + + // close the file + try + { + fs.Close(); + } + catch { } + + from?.SendMessage($"{TotalCount} spawner(s) were created from file {filename} [Trammel={TrammelCount}, Felucca={FeluccaCount}, Ilshenar={IlshenarCount}, Malas={MalasCount}, Tokuno={TokunoCount} Other={OtherCount}]."); + + if (failedobjectitemcount > 0) + { + from?.SendMessage(33, $"Failed to initialize TriggerObjects in {failedobjectitemcount} spawners. Saved to 'badxml.log'"); + } + if (failedsetitemcount > 0) + { + from?.SendMessage(33, $"Failed to initialize SetItemProperty Objects in {failedsetitemcount} spawners. Saved to 'badxml.log'"); + } + if (badcount > 0) + { + from?.SendMessage(33, $"{badcount} bad spawners detected. Saved to 'badxml.log'"); + } + if (questionablecount > 0) + { + from?.SendMessage(33, $"{questionablecount} questionable spawners detected. Saved to 'badxml.log'"); + } + processedmaps = 1; + processedspawners = TotalCount; + + } + + public static string LocateFile(string filename) + { + var found = false; + + string dirname = null; + + if (Directory.Exists(XmlSpawnDir)) + { + // get it from the defaults directory if it exists + dirname = $"{XmlSpawnDir}/{filename}"; + found = File.Exists(dirname) || Directory.Exists(dirname); + } + + if (!found) + { + // otherwise just get it from the main installation dir + dirname = filename; + } + + return dirname; + } + + [Usage("XmlNewLoad [SpawnerPrefixFilter]")] + [Description("Loads new XmlSpawner objects with new GUIDs (no replacement) into the current map of the player.")] + public static void NewLoad_OnCommand(CommandEventArgs e) + { + if (e.Mobile.AccessLevel >= DiskAccessLevel) + { + if (e.Arguments.Length >= 1) + { + var filename = LocateFile(e.Arguments[0]); + + // Spawner load criteria (if any) + var SpawnerPrefix = string.Empty; + + // Check if there is an argument provided (load criteria) + if (e.Arguments.Length > 1) + { + SpawnerPrefix = e.Arguments[1]; + } + + XmlLoadFromFile(filename, SpawnerPrefix, e.Mobile, false, 0, true, out _, out _); + } + else + { + e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter]"); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + [Usage("XmlLoad [SpawnerPrefixFilter]")] + [Description("Loads XmlSpawner objects (replacing existing spawners with matching GUIDs) into the proper map as defined in the file supplied.")] + public static void Load_OnCommand(CommandEventArgs e) + { + var m = e.Mobile; + + if (m == null || m.AccessLevel >= DiskAccessLevel) + { + if (e.Arguments.Length >= 1) + { + var filename = LocateFile(e.Arguments[0]); + + // Spawner load criteria (if any) + var SpawnerPrefix = string.Empty; + + // Check if there is an argument provided (load criteria) + if (e.Arguments.Length > 1) + { + SpawnerPrefix = e.Arguments[1]; + } + + XmlLoadFromFile(filename, SpawnerPrefix, m, false, 0, false, out _, out _); + } + else if (m != null) + { + e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter]"); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + [Usage("XmlNewLoadHere [SpawnerPrefixFilter][-maxrange range]")] + [Description("Loads new XmlSpawner objects with new GUIDs (no replacement) to the current map and location of the player. Spawners beyond maxrange (default=48 tiles) are not moved relative to the player")] + public static void NewLoadHere_OnCommand(CommandEventArgs e) + { + if (e.Mobile.AccessLevel >= DiskAccessLevel) + { + if (e.Arguments.Length >= 1) + { + var filename = LocateFile(e.Arguments[0]); + + // Spawner load criteria (if any) + var SpawnerPrefix = string.Empty; + var badargs = false; + var maxrange = 48; + + // Check if there is an argument provided (load criteria) + try + { + // Check if there is an argument provided (load criteria) + for (var nxtarg = 1; nxtarg < e.Arguments.Length; nxtarg++) + { + // is it a maxrange option? + if (e.Arguments[nxtarg].ToLower() == "-maxrange") + { + maxrange = int.Parse(e.Arguments[++nxtarg]); + } + else + { + SpawnerPrefix = e.Arguments[nxtarg]; + } + } + } + catch { e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter][-maxrange range]"); badargs = true; } + + if (!badargs) + { + XmlLoadFromFile(filename, SpawnerPrefix, e.Mobile, true, maxrange, true, out _, out _); + } + } + else + { + e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter][-maxrange range]"); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + [Usage("XmlLoadHere [SpawnerPrefixFilter][-maxrange range]")] + [Description("Loads XmlSpawner objects to the current map and location of the player. Spawners beyond maxrange (default=48 tiles) are not moved relative to the player")] + public static void LoadHere_OnCommand(CommandEventArgs e) + { + if (e.Mobile.AccessLevel >= DiskAccessLevel) + { + if (e.Arguments.Length >= 1) + { + var filename = LocateFile(e.Arguments[0]); + + // Spawner load criteria (if any) + var SpawnerPrefix = string.Empty; + var badargs = false; + var maxrange = 48; + + try + { + // Check if there is an argument provided (load criteria) + for (var nxtarg = 1; nxtarg < e.Arguments.Length; nxtarg++) + { + // is it a maxrange option? + if (e.Arguments[nxtarg].ToLower() == "-maxrange") + { + maxrange = int.Parse(e.Arguments[++nxtarg]); + } + else + { + SpawnerPrefix = e.Arguments[nxtarg]; + } + } + } + catch { e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter][-maxrange range]"); badargs = true; } + + if (!badargs) + { + XmlLoadFromFile(filename, SpawnerPrefix, e.Mobile, true, maxrange, false, out _, out _); + } + } + else + { + e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter][-maxrange range]"); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + [Usage("XmlSaveOld [SpawnerPrefixFilter]")] + [Description("Saves all XmlSpawner objects from the current map into the file supplied in the old xmlspawner format.")] + public static void SaveOld_OnCommand(CommandEventArgs e) + { + SaveSpawns(e, false, true); + } + + [Usage("XmlSpawnerSave [SpawnerPrefixFilter]")] + [Description("Saves all XmlSpawner objects from the current map into the file supplied.")] + public static void Save_OnCommand(CommandEventArgs e) + { + SaveSpawns(e, false, false); + } + + [Usage("XmlSpawnerSaveAll [SpawnerPrefixFilter]")] + [Description("Saves ALL XmlSpawner objects from the entire world into the file supplied.")] + public static void SaveAll_OnCommand(CommandEventArgs e) + { + SaveSpawns(e, true, false); + } + + public class XmlSaveSingle : BaseCommand + { + public XmlSaveSingle() + { + AccessLevel = DiskAccessLevel; + Supports = CommandSupport.Single; + Commands = new[] { "XmlSaveSingle" }; + ObjectTypes = ObjectTypes.Items; + Usage = "XmlSaveSingle "; + Description = "Saves single xmlspawner to specified file."; + } + + public override void Execute(CommandEventArgs e, object obj) + { + if (e?.Mobile == null || e.Arguments == null) + { + return; + } + + if (e.Arguments.Length < 1) + { + e.Mobile.SendMessage($"Usage: {e.Command} (without spaces!!)"); + return; + } + + var filename = e.Arguments[0]; + + if (obj is not XmlSpawner xmlspawner) + { + e.Mobile.SendMessage("You can select only XmlSpawner objects!"); + return; + } + + var m = e.Mobile; + + CommandLogging.WriteLine(m, $"{m.AccessLevel} {CommandLogging.Format(m)} Saving XmlSpawner {CommandLogging.Format(xmlspawner)} on file {CommandLogging.Format(filename)}"); + SaveSpawns(m, xmlspawner, filename); + } + } + + private static void SaveSpawns(Mobile m, XmlSpawner xmlspawner, string filename) + { + if (m.AccessLevel < DiskAccessLevel) + { + m.SendMessage("You do not have rights to perform this command."); + return; + } + + string dirname; + + if (Directory.Exists(XmlSpawnDir) && filename?.StartsWith("/") == false && !filename.StartsWith("\\")) + { + // put it in the defaults directory if it exists + dirname = $"{XmlSpawnDir}/{filename}"; + } + else + { + // otherwise just put it in the main installation dir + dirname = filename; + } + + m.SendMessage($"Saving object in folder {dirname} - file {filename} - spawner {xmlspawner}."); + + var saveslist = new List(1) + { + xmlspawner + }; + _ = SaveSpawnList(m, saveslist, dirname, false, true); + } + + private static void SaveSpawns(CommandEventArgs e, bool SaveAllMaps, bool oldformat) + { + if (e?.Mobile == null || e.Arguments == null || e.Arguments.Length < 1) + { + return; + } + + if (e.Mobile.AccessLevel < DiskAccessLevel) + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + return; + } + + if (e.Arguments?.Length < 1) + { + e.Mobile.SendMessage($"Usage: {e.Command} [SpawnerPrefixFilter]"); + return; + } + + // Spawner save criteria (if any) + var SpawnerPrefix = string.Empty; + + // Check if there is an argument provided (save criteria) + if (e.Arguments.Length > 1) + { + SpawnerPrefix = e.Arguments[1]; + } + + var filename = e.Arguments[0]; + + string dirname; + if (Directory.Exists(XmlSpawnDir) && filename?.StartsWith("/") == false && !filename.StartsWith("\\")) + { + // put it in the defaults directory if it exists + dirname = $"{XmlSpawnDir}/{filename}"; + } + else + { + // otherwise just put it in the main installation dir + dirname = filename; + } + + if (SaveAllMaps) + { + e.Mobile.SendMessage( + $"Saving XmlSpawner objects{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)} to file {dirname} from {e.Mobile.Map}." + ); + } + else + { + e.Mobile.SendMessage( + $"Saving XmlSpawner obejcts{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)} to file {dirname} from the entire world." + ); + } + + var saveslist = new List(); + + // Add each spawn point to the list + foreach (var i in World.Items.Values) + { + if (i is XmlSpawner spawner && !spawner.Deleted && (SaveAllMaps || spawner.Map == e.Mobile.Map) + //check for mob carried spawners and ignore them + && spawner.RootParent is not Mobile + && (SpawnerPrefix == null || SpawnerPrefix.Length == 0 || spawner.Name?.StartsWith(SpawnerPrefix) == true)) + { + saveslist.Add(spawner); + } + } + + // save the list + _ = SaveSpawnList(e.Mobile, saveslist, dirname, oldformat, true); + } + + public static bool SaveSpawnList(List savelist, Stream stream) => SaveSpawnList(null, savelist, null, stream, false, false); + + public static bool SaveSpawnList(Mobile from, List savelist, string dirname, bool oldformat, bool verbose) + { + if (string.IsNullOrEmpty(dirname)) + { + return false; + } + + var save_ok = true; + FileStream fs = null; + + try + { + // Create the FileStream to write with. + fs = new FileStream(dirname, FileMode.Create); + } + catch + { + from?.SendMessage($"Error creating file {dirname}"); + + save_ok = false; + } + + // so far so good + if (save_ok) + { + save_ok = SaveSpawnList(from, savelist, dirname, fs, oldformat, verbose); + } + + if (!save_ok && from != null) + { + from.SendMessage("Unable to complete save operation."); + } + + return save_ok; + } + + public static bool SaveSpawnList(Mobile from, List savelist, string dirname, Stream stream, bool oldformat, bool verbose) + { + if (savelist == null || stream == null) + { + return false; + } + + var TotalCount = 0; + var TrammelCount = 0; + var FeluccaCount = 0; + var IlshenarCount = 0; + var MalasCount = 0; + var TokunoCount = 0; + var OtherCount = 0; + + // Create the data set + var ds = new DataSet(SpawnDataSetName); + + // Load the data set up + _ = ds.Tables.Add(SpawnTablePointName); + + // Create spawn point schema + _ = ds.Tables[SpawnTablePointName].Columns.Add("Name"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("UniqueId"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("Map"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("X"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("Y"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("Width"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("Height"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("CentreX"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("CentreY"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("CentreZ"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("Range"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MaxCount"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MinDelay"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MaxDelay"); + // deal with the double format for delay. old format stored them as minutes in int format. that meant that short delays were lost + // proper solution would simply be to store as doubles, but older progs still assume int format (like spawneditor) + // so this is the solution. add a flag and do it both ways. + _ = ds.Tables[SpawnTablePointName].Columns.Add("DelayInSec"); + + // add the duration and proximity range and sound parameters, and in container flag and coords inside the container + _ = ds.Tables[SpawnTablePointName].Columns.Add("Duration"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("DespawnTime"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ProximityRange"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ProximityTriggerSound"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ProximityTriggerMessage"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ObjectPropertyName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ObjectPropertyItemName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("SetPropertyItemName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ItemTriggerName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("NoItemTriggerName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MobTriggerName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MobPropertyName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("PlayerPropertyName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("TriggerProbability"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("SpeechTrigger"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("SkillTrigger"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("InContainer"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ContainerX"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ContainerY"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ContainerZ"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MinRefractory"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("MaxRefractory"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("TODStart"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("TODEnd"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("TODMode"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("KillReset"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ExternalTriggering"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("SequentialSpawning"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("RegionName"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("AllowGhostTriggering"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("AllowNPCTriggering"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("SpawnOnTrigger"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("ConfigFile"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("SmartSpawning"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("TickReset"); + + _ = ds.Tables[SpawnTablePointName].Columns.Add("WayPoint"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("Team"); + // amount for stacked item spawns + _ = ds.Tables[SpawnTablePointName].Columns.Add("Amount"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("IsGroup"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("IsRunning"); + _ = ds.Tables[SpawnTablePointName].Columns.Add("IsHomeRangeRelative"); + _ = ds.Tables[SpawnTablePointName].Columns.Add(oldformat ? "Objects" : "Objects2"); + + // Always export sorted by UUID to help diffs + savelist.Sort((a, b) => + { + return a.UniqueId.CompareTo(b.UniqueId); + }); + + // Add each spawn point to the new table + foreach (var sp in savelist) + { + if (sp?.Map == null || sp.Deleted) + { + continue; + } + + // Send a message to the client that the spawner is being saved + if (verbose && from != null) + { + from.SendMessage(68, $"Saving '{sp.Name}' in {sp.Map.Name} at {sp.Location}"); + } + + // Create a new data row + var dr = ds.Tables[SpawnTablePointName].NewRow(); + + // Populate the data + dr["Name"] = sp.Name; + + // Set the unqiue id + dr["UniqueId"] = sp.UniqueId; + + // Get the map name + dr["Map"] = sp.Map.Name; + + // Convert the xml map value to a real map object + if (sp.Map.Name.InsensitiveEquals(Map.Trammel.Name)) + { + TrammelCount++; + } + else if (sp.Map.Name.InsensitiveEquals(Map.Felucca.Name)) + { + FeluccaCount++; + } + else if (sp.Map.Name.InsensitiveEquals(Map.Ilshenar.Name)) + { + IlshenarCount++; + } + else if (sp.Map.Name.InsensitiveEquals(Map.Malas.Name)) + { + MalasCount++; + } + else if (sp.Map.Name.InsensitiveEquals(Map.Tokuno.Name)) + { + TokunoCount++; + } + else + { + OtherCount++; + } + + dr["X"] = sp.m_X; + dr["Y"] = sp.m_Y; + dr["Width"] = sp.m_Width; + dr["Height"] = sp.m_Height; + + // check to see if this is in a container + if (sp.RootParent is Container container) + { + dr["CentreX"] = container.Location.X; + dr["CentreY"] = container.Location.Y; + dr["CentreZ"] = container.Location.Z; + dr["ContainerX"] = sp.Location.X; + dr["ContainerY"] = sp.Location.Y; + dr["ContainerZ"] = sp.Location.Z; + dr["InContainer"] = true; + } + else + { + dr["CentreX"] = sp.Location.X; + dr["CentreY"] = sp.Location.Y; + dr["CentreZ"] = sp.Location.Z; + //dr["ContainerX"] = 0; + //dr["ContainerY"] = 0; + //dr["ContainerZ"] = 0; + dr["InContainer"] = false; + } + dr["Range"] = sp.m_HomeRange; + dr["MaxCount"] = sp.m_Count; + + // need to deal with the fact that the old xmlspawner xml format only saved delays in minutes as ints, so shorter spawn times + // are lost + // flag it then on reading it can be properly handled and still + // maintain backward compatibility with older xml files + if ((int)sp.m_MinDelay.TotalSeconds - 60 * (int)sp.m_MinDelay.TotalMinutes > 0 || + (int)sp.m_MaxDelay.TotalSeconds - 60 * (int)sp.m_MaxDelay.TotalMinutes > 0) + { + dr["DelayInSec"] = true; + dr["MinDelay"] = (int)sp.m_MinDelay.TotalSeconds; + dr["MaxDelay"] = (int)sp.m_MaxDelay.TotalSeconds; + } + else + { + dr["DelayInSec"] = false; + dr["MinDelay"] = (int)sp.m_MinDelay.TotalMinutes; + dr["MaxDelay"] = (int)sp.m_MaxDelay.TotalMinutes; + } + + // additional parameters + dr["TODStart"] = sp.TODStart.TotalMinutes; + dr["TODEnd"] = sp.TODEnd.TotalMinutes; + dr["TODMode"] = (int)sp.TODMode; + dr["KillReset"] = sp.KillReset; + dr["MinRefractory"] = sp.RefractMin.TotalMinutes; + dr["MaxRefractory"] = sp.RefractMax.TotalMinutes; + dr["Duration"] = sp.m_Duration.TotalMinutes; + dr["DespawnTime"] = sp.DespawnTime.TotalHours; + dr["ExternalTriggering"] = sp.ExternalTriggering; + + dr["ProximityRange"] = sp.m_ProximityRange; + dr["ProximityTriggerSound"] = sp.ProximitySound; + dr["ProximityTriggerMessage"] = sp.ProximityMsg; + if (sp.m_ObjectPropertyItem?.Deleted == false) + { + dr["ObjectPropertyItemName"] = $"{sp.m_ObjectPropertyItem.Name},{sp.m_ObjectPropertyItem.GetType().Name}"; + } + else + { + dr["ObjectPropertyItemName"] = null; + } + + dr["ObjectPropertyName"] = sp.m_ObjectPropertyName; + if (sp.SetItem?.Deleted == false) + { + dr["SetPropertyItemName"] = $"{sp.SetItem.Name},{sp.SetItem.GetType().Name}"; + } + else + { + dr["SetPropertyItemName"] = null; + } + + dr["ItemTriggerName"] = sp.m_ItemTriggerName; + dr["NoItemTriggerName"] = sp.m_NoItemTriggerName; + dr["MobTriggerName"] = sp.MobTriggerName; + dr["MobPropertyName"] = sp.MobTriggerProp; + dr["PlayerPropertyName"] = sp.PlayerTriggerProp; + dr["TriggerProbability"] = sp.TriggerProbability; + dr["SequentialSpawning"] = sp.SequentialSpawn; + dr["RegionName"] = sp.m_RegionName; + dr["AllowGhostTriggering"] = sp.AllowGhostTrig; + dr["AllowNPCTriggering"] = sp.AllowNPCTrig; + dr["SpawnOnTrigger"] = sp.SpawnOnTrigger; + dr["ConfigFile"] = sp.ConfigFile; + dr["SmartSpawning"] = sp.m_SmartSpawning; + dr["TickReset"] = sp.DisableGlobalAutoReset; + + dr["SpeechTrigger"] = sp.SpeechTrigger; + dr["SkillTrigger"] = sp.SkillTrigger; + dr["Amount"] = sp.StackAmount; + dr["Team"] = sp.m_Team; + + // assign the waypoint based on the waypoint name if it deviates from the default waypoint name, otherwise do it by serial + string waystr = null; + if (sp.WayPoint != null) + { + if (sp.WayPoint.Name != defwaypointname && !string.IsNullOrEmpty(sp.WayPoint.Name)) + { + waystr = sp.WayPoint.Name; + } + else + { + waystr = $"SERIAL,{sp.WayPoint.Serial}"; + } + } + dr["WayPoint"] = waystr; + + dr["IsGroup"] = sp.m_Group; + dr["IsRunning"] = sp.m_Running; + dr["IsHomeRangeRelative"] = sp.HomeRangeIsRelative; + if (oldformat) + { + dr["Objects"] = sp.GetSerializedObjectList(); + } + else + { + dr["Objects2"] = sp.GetSerializedObjectList2(); + } + + // Add the row the the table + ds.Tables[SpawnTablePointName].Rows.Add(dr); + + // Increment the count + TotalCount++; + } + + // Write out the file + var file_error = false; + if (TotalCount > 0) + { + try + { + ds.WriteXml(stream); + } + catch { file_error = true; } + + if (file_error) + { + return false; + } + } + + try + { + stream.Close(); + } + catch { } + // Indicate how many spawners were written + from?.SendMessage($"{TotalCount} spawner(s) were saved to file {dirname} [Trammel={TrammelCount:N0}, Felucca={FeluccaCount:N0}, Ilshenar={IlshenarCount:N0}, Malas={MalasCount:N0}, Tokuno={TokunoCount:N0}, Other={OtherCount:N0}]."); + return true; + + } + + private static void WipeSpawners(CommandEventArgs e, bool WipeAll) + { + if (e?.Mobile == null) + { + return; + } + + if (e.Mobile.AccessLevel >= AccessLevel.Administrator) + { + // Spawner delete criteria (if any) + var SpawnerPrefix = string.Empty; + + // Check if there is an argument provided (delete criteria) + if (e.Arguments?.Length > 0) + { + SpawnerPrefix = e.Arguments[0]; + } + + if (WipeAll) + { + e.Mobile.SendMessage($"Removing ALL XmlSpawner objects from the world{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)}."); + } + else + { + e.Mobile.SendMessage($"Removing ALL XmlSpawner objects from {e.Mobile.Map}{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)}."); + } + + // Delete Xml spawner's in the world based on the mobiles current map + var Count = 0; + var ToDelete = new List(); + foreach (var i in World.Items.Values) + { + if (i is XmlSpawner && (WipeAll || i.Map == e.Mobile.Map) && i.Deleted == false) + { + // Check if there is a delete condition + if (SpawnerPrefix == null || SpawnerPrefix.Length == 0 || i.Name.StartsWith(SpawnerPrefix)) + { + // Send a message to the client that the spawner is being deleted + //e.Mobile.SendMessage(33, "Removing '{0}' in {1} at {2}", i.Name, i.Map.Name, i.Location.ToString()); + + ToDelete.Add(i); + Count++; + } + } + } + + // Delete the items in the array list + foreach (var i in ToDelete) + { + i.Delete(); + } + + if (WipeAll) + { + e.Mobile.SendMessage($"Removed {Count:N0} XmlSpawner objects from the world."); + } + else + { + e.Mobile.SendMessage($"Removed {Count:N0} XmlSpawner objects from {e.Mobile.Map}."); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + + [Usage("XmlSpawnerRespawn [SpawnerPrefixFilter]")] + [Description("Respawns all XmlSpawner objects from the current map.")] + public static void Respawn_OnCommand(CommandEventArgs e) + { + RespawnSpawners(e, false); + } + + [Usage("XmlSpawnerRespawnAll [SpawnerPrefixFilter]")] + [Description("Respawns all XmlSpawner objects from the entire world.")] + public static void RespawnAll_OnCommand(CommandEventArgs e) + { + RespawnSpawners(e, true); + } + + private static void RespawnSpawners(CommandEventArgs e, bool RespawnAll) + { + if (e?.Mobile == null) + { + return; + } + + if (e.Mobile.AccessLevel >= AccessLevel.Administrator) + { + // Spawner Respawn criteria (if any) + var SpawnerPrefix = string.Empty; + + // Check if there is an argument provided (respawn criteria) + if (e.Arguments?.Length > 0) + { + SpawnerPrefix = e.Arguments[0]; + } + + if (RespawnAll) + { + e.Mobile.SendMessage($"Respawning ALL XmlSpawner objects from the world{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)}."); + } + else + { + e.Mobile.SendMessage($"Respawning ALL XmlSpawner objects from {e.Mobile.Map}{(!string.IsNullOrEmpty(SpawnerPrefix) ? $" beginning with {SpawnerPrefix}" : string.Empty)}."); + } + + // Respawn Xml spawner's in the world based on the mobiles current map + var Count = 0; + var ToRespawn = new List(); + foreach (var i in World.Items.Values) + { + try + { + if (i is XmlSpawner && (RespawnAll || i.Map == e.Mobile.Map) && i.Deleted == false) + { + // Check if there is a respawn condition + if (SpawnerPrefix == null || SpawnerPrefix.Length == 0 || i.Name?.StartsWith(SpawnerPrefix) == true) + { + ToRespawn.Add(i); + Count++; + } + } + } + catch (Exception ex) { Console.WriteLine("Error attempting to add {0}, {1}", i, ex.Message); } + } + // Respawn the items in the array list + foreach (var i in ToRespawn) + { + + // Send a message to the client that the spawner is being respawned + e.Mobile.SendMessage(33, $"Respawning '{i.Name}' in {i.Map.Name} at {i.Location}"); + var CheckXmlSpawner = (XmlSpawner)i; + _ = CheckXmlSpawner.TryRespawn(); + } + + if (RespawnAll) + { + e.Mobile.SendMessage($"Respawned {Count:N0} XmlSpawner objects from the world."); + } + else + { + e.Mobile.SendMessage($"Respawned {Count:N0} XmlSpawner objects from {e.Mobile.Map}."); + } + } + else + { + e.Mobile.SendMessage("You do not have rights to perform this command."); + } + } + +#if TRACE + public static void XmlMake_OnCommand(CommandEventArgs e) + { + + if (e.Arguments.Length > 0) + { + var count = 0; + try + { + count = Convert.ToInt32(e.Arguments[0], 10); + } + catch (Exception ex) { Diagnostics.ExceptionLogging.LogException(ex); } + + for (var i = 0; i < count; i++) + { + if (e.Arguments.Length > 2) + { + _ = new Spawner(10, 1, 1, 0, 2, e.Arguments[1]) + { + Location = new Point3D(5400 + Utility.Random(700), 1090 + Utility.Random(180), 0), + Map = Map.Trammel + }; + } + else + if (e.Arguments.Length > 1) + { + _ = new XmlSpawner(10, 1, 1, 0, 2, e.Arguments[1]) + { + Location = new Point3D(5400 + Utility.Random(700), 1090 + Utility.Random(180), 0), + Map = Map.Trammel + }; + //x.MinDelay = TimeSpan.FromSeconds(1); + //x.MaxDelay = TimeSpan.FromSeconds(1); + //x.ProximityRange = 0; + } + } + if (e.Arguments.Length > 2) + { + e.Mobile.SendMessage($"Created {count} Spawner objects."); + } + else + { + e.Mobile.SendMessage($"Created {count} XmlSpawner objects."); + } + + } + } + + public static void XmlTrace_OnCommand() + { + XmlTrace_OnCommand(null); + } + + public static void XmlTrace_OnCommand(CommandEventArgs e) + { + var currentprocess = Process.GetCurrentProcess(); + var runningtime = Core.Now - _traceStartTime; + var processtime = currentprocess.UserProcessorTime.TotalMilliseconds - _startProcessTime; + double sysload = 0; + + if (runningtime.TotalMilliseconds > 0) + { + sysload = processtime / runningtime.TotalMilliseconds; + } + + Console.WriteLine("______________"); + Console.WriteLine("Active Traces:"); + Console.WriteLine("Running Time = {0}", runningtime); + Console.WriteLine("Adjusted Process Time = {0:####.####} secs", processtime / 1000); + Console.WriteLine("Processor Time = {0} ({1:p3} avg sys load)", currentprocess.UserProcessorTime, sysload); + + for (var i = 0; i < _traceCount.Length; i++) + { + if (_traceCount[i] > 0) + { + double load = 0; + if (processtime > 0) + { + load = _traceTotal[i].TotalMilliseconds / processtime; + } + Console.WriteLine("{0} ({4}) {1,21} / {2} calls = {3:####.####} ms/call, {5:p3}", + i, _traceTotal[i], _traceCount[i], _traceTotal[i].TotalMilliseconds / _traceCount[i], + _traceName[i], load); + } + } + } + + public static void XmlResetTrace_OnCommand(CommandEventArgs e) + { + if (e.Arguments.Length >= 0) + { + for (var i = 0; i < _traceCount.Length; i++) + { + _traceCount[i] = 0; + _traceTotal[i] = TimeSpan.Zero; + } + _traceStartTime = Core.Now; + + var currentprocess = Process.GetCurrentProcess(); + _startProcessTime = currentprocess.UserProcessorTime.TotalMilliseconds; + + Console.WriteLine("Traces reset"); + } + } +#endif + + [Constructible] + public XmlSpawner() + : base(BaseItemId) + { + PlayerCreated = true; + UniqueId = Guid.NewGuid().ToString(); + SpawnRange = defSpawnRange; + + InitSpawn(0, 0, m_Width, m_Height, string.Empty, 0, defMinDelay, defMaxDelay, defDuration, + defProximityRange, defProximityTriggerSound, defAmount, defTeam, defHomeRange, defRelativeHome, Array.Empty(), defMinRefractory, defMaxRefractory, + defTODStart, defTODEnd, null, null, null, null, null, null, null, null, null, defTriggerProbability, null, defIsGroup, defTODMode, + defKillReset, false, -1, null, false, false, false, null, defDespawnTime, null, false, null); + } + + [Constructible] + public XmlSpawner(int amount, int minDelay, int maxDelay, int team, int homeRange, string creatureName) + : base(BaseItemId) + { + PlayerCreated = true; + UniqueId = Guid.NewGuid().ToString(); + SpawnRange = homeRange; + var so = new SpawnObject[1]; + so[0] = new SpawnObject(creatureName, amount); + + InitSpawn(0, 0, m_Width, m_Height, string.Empty, amount, TimeSpan.FromMinutes(minDelay), TimeSpan.FromMinutes(maxDelay), defDuration, + defProximityRange, defProximityTriggerSound, defAmount, team, homeRange, defRelativeHome, so, defMinRefractory, defMaxRefractory, + defTODStart, defTODEnd, null, null, null, null, null, null, null, null, null, defTriggerProbability, null, defIsGroup, defTODMode, + defKillReset, false, -1, null, false, false, false, null, defDespawnTime, null, false, null); + } + + [Constructible] + public XmlSpawner(int amount, int minDelay, int maxDelay, int team, int homeRange, int spawnRange, string creatureName) + : base(BaseItemId) + { + PlayerCreated = true; + UniqueId = Guid.NewGuid().ToString(); + SpawnRange = spawnRange; + var so = new SpawnObject[1]; + so[0] = new SpawnObject(creatureName, amount); + + InitSpawn(0, 0, m_Width, m_Height, string.Empty, amount, TimeSpan.FromMinutes(minDelay), TimeSpan.FromMinutes(maxDelay), defDuration, + defProximityRange, defProximityTriggerSound, defAmount, team, homeRange, defRelativeHome, so, defMinRefractory, defMaxRefractory, + defTODStart, defTODEnd, null, null, null, null, null, null, null, null, null, defTriggerProbability, null, defIsGroup, defTODMode, + defKillReset, false, -1, null, false, false, false, null, defDespawnTime, null, false, null); + } + + [Constructible] + public XmlSpawner(string creatureName) + : base(BaseItemId) + { + PlayerCreated = true; + UniqueId = Guid.NewGuid().ToString(); + var so = new SpawnObject[1]; + so[0] = new SpawnObject(creatureName, 1); + SpawnRange = defSpawnRange; + + InitSpawn(0, 0, m_Width, m_Height, string.Empty, 1, defMinDelay, defMaxDelay, defDuration, + defProximityRange, defProximityTriggerSound, defAmount, defTeam, defHomeRange, defRelativeHome, so, defMinRefractory, defMaxRefractory, + defTODStart, defTODEnd, null, null, null, null, null, null, null, null, null, defTriggerProbability, null, defIsGroup, defTODMode, + defKillReset, false, -1, null, false, false, false, null, defDespawnTime, null, false, null); + } + + public XmlSpawner(Guid uniqueId, int x, int y, int width, int height, string name, int maxCount, TimeSpan minDelay, TimeSpan maxDelay, TimeSpan duration, + int proximityRange, int proximityTriggerSound, int amount, int team, int homeRange, bool isRelativeHomeRange, SpawnObject[] spawnObjects, + TimeSpan minRefractory, TimeSpan maxRefractory, TimeSpan todstart, TimeSpan todend, Item objectPropertyItem, string objectPropertyName, string proximityMessage, + string itemTriggerName, string noitemTriggerName, string speechTrigger, string mobTriggerName, string mobPropertyName, string playerPropertyName, double triggerProbability, + Item setPropertyItem, bool isGroup, TODModeType todMode, int killReset, bool externalTriggering, int sequentialSpawning, string regionName, + bool allowghost, bool allownpc, bool spawnontrigger, string configfile, TimeSpan despawnTime, string skillTrigger, bool smartSpawning, WayPoint wayPoint) + : base(BaseItemId) + { + UniqueId = uniqueId.ToString(); + InitSpawn(x, y, width, height, name, maxCount, minDelay, maxDelay, duration, + proximityRange, proximityTriggerSound, amount, team, homeRange, isRelativeHomeRange, spawnObjects, minRefractory, maxRefractory, todstart, todend, + objectPropertyItem, objectPropertyName, proximityMessage, itemTriggerName, noitemTriggerName, speechTrigger, mobTriggerName, mobPropertyName, playerPropertyName, + triggerProbability, setPropertyItem, isGroup, todMode, killReset, externalTriggering, sequentialSpawning, regionName, allowghost, allownpc, spawnontrigger, configfile, + despawnTime, skillTrigger, smartSpawning, wayPoint); + } + + public void InitSpawn(int x, int y, int width, int height, string name, int maxCount, TimeSpan minDelay, TimeSpan maxDelay, TimeSpan duration, + int proximityRange, int proximityTriggerSound, int amount, int team, int homeRange, bool isRelativeHomeRange, SpawnObject[] objectsToSpawn, + TimeSpan minRefractory, TimeSpan maxRefractory, TimeSpan todstart, TimeSpan todend, Item objectPropertyItem, string objectPropertyName, string proximityMessage, + string itemTriggerName, string noitemTriggerName, string speechTrigger, string mobTriggerName, string mobPropertyName, string playerPropertyName, double triggerProbability, + Item setPropertyItem, bool isGroup, TODModeType todMode, int killReset, bool externalTriggering, int sequentialSpawning, string regionName, bool allowghost, bool allownpc, bool spawnontrigger, + string configfile, TimeSpan despawnTime, string skillTrigger, bool smartSpawning, WayPoint wayPoint) + { + + Visible = false; + Movable = false; + m_X = x; + m_Y = y; + m_Width = width; + m_Height = height; + + // init spawn range if compatible + if (width == height) + { + m_SpawnRange = width / 2; + } + else + { + m_SpawnRange = -1; + } + + m_Running = true; + m_Group = isGroup; + + Name = !string.IsNullOrEmpty(name) ? name : "Spawner"; + + m_MinDelay = minDelay; + m_MaxDelay = maxDelay; + + // duration and proximity range parameter + RefractMin = minRefractory; + RefractMax = maxRefractory; + TODStart = todstart; + TODEnd = todend; + TODMode = todMode; + KillReset = killReset; + m_Duration = duration; + DespawnTime = despawnTime; + m_ProximityRange = proximityRange; + ProximitySound = proximityTriggerSound; + m_proximityActivated = false; + m_durActivated = false; + m_refractActivated = false; + m_Count = maxCount; + m_Team = team; + StackAmount = amount; + m_HomeRange = homeRange; + HomeRangeIsRelative = isRelativeHomeRange; + m_ObjectPropertyItem = objectPropertyItem; + m_ObjectPropertyName = objectPropertyName; + ProximityMsg = proximityMessage; + m_ItemTriggerName = itemTriggerName; + m_NoItemTriggerName = noitemTriggerName; + SpeechTrigger = speechTrigger; + SkillTrigger = skillTrigger; // note this will register the skill as well + MobTriggerName = mobTriggerName; + MobTriggerProp = mobPropertyName; + PlayerTriggerProp = playerPropertyName; + TriggerProbability = triggerProbability; + SetItem = setPropertyItem; + ExternalTriggering = externalTriggering; + ExtTrigState = false; + SequentialSpawn = sequentialSpawning; + RegionName = regionName; + AllowGhostTrig = allowghost; + AllowNPCTrig = allownpc; + SpawnOnTrigger = spawnontrigger; + m_SmartSpawning = smartSpawning; + ConfigFile = configfile; + WayPoint = wayPoint; + + // set the totalitem property to -1 so that it doesnt show up in the item count of containers + //TotalItems = -1; + //UpdateTotal(this, TotalType.Items, -1); + + // Create the array of spawned objects + m_SpawnObjects = new List(); + + // Assign the list of objects to spawn + SpawnObjects = objectsToSpawn; + + // Kick off the process + DoTimer(TimeSpan.FromSeconds(1)); + } + + public XmlSpawner(Serial serial) + : base(serial) + { + } + + public void Defrag(bool killtest) + { + if (m_SpawnObjects == null) + { + return; + } + + var removed = false; + var total_removed = 0; + + var deleteilist = new List(); + var deletemlist = new List(); + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + + if (o is Item item) + { + var despawned = false; + // check to see if the despawn time has elapsed. If so, then delete it if it hasnt been picked up or stolen. + if (DespawnTime.TotalHours > 0 && !item.Deleted && item.LastMoved < Core.Now - DespawnTime && item.Parent == Parent + && (!ItemFlags.GetTaken(item) || item.Parent != null && item.Parent == Parent)) // can despawn if just moved within the same container + { + //item.Delete(); + deleteilist.Add(item); + despawned = true; + } + + // Check if the items has been deleted or + // if something else now owns the item (picked it up for example) + // also check the stolen/placed in container flag. If any of those are true then the spawner doesnt own it any more so take it off the list. + // the stolen/container flag prevents spawns from being left on the list when players take them and lock them back down on the ground. + // If you have made the changes to stealing.cs and container.cs described in xmlspawner2.txt then just uncomment the line below to + // enable this check + if (item.Deleted || despawned || item.Parent != Parent // different container + || ItemFlags.GetTaken(item) && (item.Parent == null || item.Parent != Parent)) // taken and in the world, or a different container + { + _ = so.SpawnedObjects.Remove(item); + x--; + removed = true; + // if sequential spawning is active and the RestrictKillsToSubgroup flag is set, then check to see if + // the object is in the current subgroup before adding to the total + if (SequentialSpawn >= 0 && so.RestrictKillsToSubgroup) + { + if (so.SubGroup == SequentialSpawn) + { + total_removed++; + } + } + else + { + // just add it + total_removed++; + } + } + } + else if (o is Mobile mobile) + { + var despawned = false; + // check to see if the despawn time has elapsed. If so, and the sector is not active then delete it. + if (DespawnTime.TotalHours > 0 && !mobile.Deleted && mobile.Created < Core.Now - DespawnTime + && mobile.Map != null && mobile.Map != Map.Internal && !mobile.Map.GetSector(mobile.Location).Active) + { + //m.Delete(); + deletemlist.Add(mobile); + despawned = true; + } + + if (mobile.Deleted || despawned) + { + // Remove the delete mobile from the list + _ = so.SpawnedObjects.Remove(mobile); + x--; + removed = true; + // if sequential spawning is active and the RestrictKillsToSubgroup flag is set, then check to see if + // the object is in the current subgroup before adding to the total + if (SequentialSpawn >= 0 && so.RestrictKillsToSubgroup) + { + if (so.SubGroup == SequentialSpawn) + { + total_removed++; + } + } + else + { + // just add it + total_removed++; + } + } + else if (mobile is BaseCreature creature) + { + // Check if the creature has been tamed or previously tamed and released + // and if it is, remove it from the list of spawns + if (creature.Controlled || creature.IsStabled || creature.Owners?.Count > 0) + { + _ = so.SpawnedObjects.Remove(mobile); + x--; + removed = true; + // if sequential spawning is active and the RestrictKillsToSubgroup flag is set, then check to see if + // the object is in the current subgroup before adding to the total + if (SequentialSpawn >= 0 && so.RestrictKillsToSubgroup) + { + if (so.SubGroup == SequentialSpawn) + { + total_removed++; + } + } + else + { + // just add it + total_removed++; + } + } + } + } + else + if (o is BaseXmlSpawner.KeywordTag tag) + { + if (tag.Deleted) + { + _ = so.SpawnedObjects.Remove(o); + x--; + removed = true; + } + } + else + { + // Don't know what this is, so remove it + Console.WriteLine("removing unknown {0} from spawnlist", so); + _ = so.SpawnedObjects.Remove(o); + x--; + removed = true; + } + } + } + + DeleteFromList(deleteilist, deletemlist); + + // Check if anything has been removed + if (removed) + { + InvalidateProperties(); + } + + // increment the killcount based upon the number of items that were removed from the spawnlist (i.e. were spawned but now are gone, presumed killed) + if (killtest) + { + m_killcount += total_removed; + } + } + + // special defrag pass to remove GOTO keyword tags + public void ClearGOTOTags() + { + if (m_SpawnObjects == null) + { + return; + } + + var ToDelete = new List(); + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + if (o is BaseXmlSpawner.KeywordTag sot) + { + // clear the tags except for gump and delay tags + if (sot.Type == 2) + { + ToDelete.Add(sot); + _ = so.SpawnedObjects.Remove(o); + x--; + } + + } + } + } + + for (var x = ToDelete.Count - 1; x >= 0; --x) //BaseXmlSpawner.KeywordTag i in ToDelete) + { + var i = ToDelete[x]; + if (i?.Deleted == false) + { + i.Delete(); + } + } + } + + // special defrag pass to remove spawn object tags, which are placeholders for the special keyword spawn spec entries + public void ClearTags(bool all) + { + if (m_SpawnObjects == null) + { + return; + } + + var removed = false; + var ToDelete = new List(); + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + if (o is BaseXmlSpawner.KeywordTag sot) + { + // clear the tags except for gump and delay tags + if (all || (sot.Flags & BaseXmlSpawner.KeywordFlags.Defrag) != 0) + { + ToDelete.Add(sot); + _ = so.SpawnedObjects.Remove(o); + x--; + removed = true; + } + + } + } + } + + for (var x = ToDelete.Count - 1; x >= 0; --x) //each (BaseXmlSpawner.KeywordTag i in ToDelete) + { + var i = ToDelete[x]; + if (i?.Deleted == false) + { + i.Delete(); + } + } + + // full clear of the taglist + if (all) + { + m_KeywordTagList.Clear(); + } + + // Check if anything has been removed + if (removed) + { + InvalidateProperties(); + } + } + + public void DeleteGumpTags() + { + if (m_SpawnObjects == null) + { + return; + } + + var removed = false; + var ToDelete = new List(); + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + if (o is BaseXmlSpawner.KeywordTag sot) + { + // clear the gump tags + if (sot.Type == 1) + { + ToDelete.Add(sot); + _ = so.SpawnedObjects.Remove(o); + x--; + removed = true; + } + } + } + } + + for (var x = ToDelete.Count - 1; x >= 0; --x) //BaseXmlSpawner.KeywordTag i in ToDelete) + { + var i = ToDelete[x]; + if (i?.Deleted == false) + { + i.Delete(); + } + } + + // Check if anything has been removed + if (removed) + { + InvalidateProperties(); + } + } + + public void DeleteTag(BaseXmlSpawner.KeywordTag tag) + { + if (m_SpawnObjects == null) + { + return; + } + + var removed = false; + var ToDelete = new List(); + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + if (o is BaseXmlSpawner.KeywordTag sot) + { + // clear the matching tags + if (sot == tag) + { + ToDelete.Add(sot); + _ = so.SpawnedObjects.Remove(o); + x--; + removed = true; + } + } + } + } + + for (var x = ToDelete.Count - 1; x >= 0; --x) //BaseXmlSpawner.KeywordTag i in ToDelete) + { + var i = ToDelete[x]; + if (i?.Deleted == false) + { + i.Delete(); + } + } + + // Check if anything has been removed + if (removed) + { + InvalidateProperties(); + } + } + + private int SubGroupCount(int sgroup) + { + if (m_SpawnObjects == null) + { + return 0; + } + + var nsub = 0; + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var s = m_SpawnObjects[i]; + + if (s.SubGroup == sgroup) + { + nsub++; + } + } + + return nsub; + } + + private int RandomAvailableSpawnIndex() => + // get spawn indices randomly from all available spawns independent of group + RandomAvailableSpawnIndex(-1); + + // get spawn indices randomly from all available spawns of a group + private int RandomAvailableSpawnIndex(int sgroup) + { + if (m_SpawnObjects == null) + { + return -1; + } + + var maxrange = 0; + List sgrouplist = null; + var totalcount = 0; + // make a pass to determine which subgroups are available for spawning + // by finding any subgroups that do not have available spawns + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var s = m_SpawnObjects[i]; + if (s.SubGroup > 0 && (s.Ignore || s.Disabled)) + { + continue; + } + + totalcount += s.SpawnedObjects.Count; + if (s.SubGroup > 0 && s.SpawnedObjects.Count >= s.MaxCount) + { + // this subgroup is not available so add it to the list + sgrouplist ??= new List(); + sgrouplist.Add(s.SubGroup); + } + } + + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var s = m_SpawnObjects[i]; + + if (s.SubGroup > 0 && (s.Ignore || s.Disabled)) + { + continue; + } + + if (s.MaxCount > s.SpawnedObjects.Count && (sgroup < 0 || sgroup == s.SubGroup) + && (sgrouplist == null || !sgrouplist.Contains(s.SubGroup)) && (s.SubGroup <= 0 || SubGroupCount(s.SubGroup) + totalcount <= MaxCount)) + { + // keep track of the number of spawn objects that are not at max (hence available for spawning) + // this will be used to compute the probabilistic weighting function based on the relative + // maxcounts of each entry + maxrange += s.MaxCount; + s.Available = true; + } + else + { + s.Available = false; + } + } + // now generate a random number over the available spawnobjects + // but only if the entire subgroup is available for spawning + // note, subgroup zero is exempt from this check. + if (maxrange > 0) + { + var randindex = Utility.Random(maxrange); + + // and map it into the avail spawns + var currentrange = 0; + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var s = m_SpawnObjects[i]; + if (s.SubGroup > 0 && (s.Ignore || s.Disabled)) + { + continue; + } + + // keep track of the number of spawn objects that are not at max (hence available for spawning) + if (s.Available) + { + // check to see if the random value maps into the range of the current index + if (randindex >= currentrange && randindex < currentrange + s.MaxCount) + { + return i; + } + + currentrange += s.MaxCount; + } + } + + // should never get here + return -1; + } + + // no spawns are available + return -1; + } + + // get spawn indices randomly from all available spawns of a group + private int RandomSpawnIndex(int sgroup) + { + if (m_SpawnObjects == null) + { + return -1; + } + + var avail = 0; + var maxrange = 0; + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var s = m_SpawnObjects[i]; + + // keep track of the number of spawn objects that are not at max (hence available for spawning) + if (sgroup < 0 || sgroup == s.SubGroup) + { + avail++; + maxrange += s.MaxCount; + } + } + // now generate a random number over the available spawnobjects + if (avail > 0 && maxrange > 0) + { + var randindex = Utility.Random(maxrange); + + // and map it into the avail spawns + var currentrange = 0; + + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var s = m_SpawnObjects[i]; + + // keep track of the number of spawn objects that are not at max (hence available for spawning) + if (sgroup < 0 || sgroup == s.SubGroup) + { + if (randindex >= currentrange && randindex < currentrange + s.MaxCount) + { + return i; + } + + currentrange += s.MaxCount; + } + } + + // should never get here + return -1; + } + + // no spawns are available + return -1; + } + + // return the next subgroup in the sequence. + public int NextSequentialIndex(int sgroup) + { + if (m_SpawnObjects == null || m_SpawnObjects.Count == 0) + { + return 0; + } + + var finddirection = 1; + var largergroup = -1; + + //find the next subgroup that is greater than the current one + for (var j = 0; j < m_SpawnObjects.Count; j++) + { + var s = m_SpawnObjects[j]; + if (s.SubGroup > 0 && (s.Ignore || s.Disabled)) + { + continue; + } + + var thisgroup = s.SubGroup; + + // start off by finding a subgroup that is larger + if (finddirection == 1) + { + if (thisgroup > sgroup) + { + largergroup = thisgroup; + + // then work backward to find the group that is less than this but still larger than the current + finddirection = -1; + } + } + else + { + if (thisgroup > sgroup && thisgroup < largergroup) + { + largergroup = thisgroup; + + finddirection = -1; + } + } + } + + // if couldnt find one larger, then it is time to wraparound + if (largergroup < 0 && sgroup >= 0) + { + return NextSequentialIndex(-1); + } + + return largergroup; + } + + // returns the spawn index of a spawn entry in the current sequential subgroup + public int GetCurrentAvailableSequentialSpawnIndex(int sgroup) + { + if (sgroup < 0) + { + return -1; + } + + if (m_SpawnObjects == null) + { + return -1; + } + + if (sgroup == 0) + { + return RandomAvailableSpawnIndex(0); + } + + //return the first instance of a spawn object that is an available member of the requested subgroup + for (var j = 0; j < m_SpawnObjects.Count; j++) + { + var s = m_SpawnObjects[j]; + + if (s.SubGroup == sgroup && s.MaxCount > s.SpawnedObjects.Count) + { + return j; + } + } + // failed to find any spawn entry of the requested subgroup + return -1; + } + + // returns the spawn index of a spawn entry in the current sequential subgroup + public int GetCurrentSequentialSpawnIndex(int sgroup) + { + if (sgroup < 0) + { + return -1; + } + + if (m_SpawnObjects == null) + { + return -1; + } + + if (sgroup == 0) + { + return RandomSpawnIndex(0); + } + + //return the first instance of a spawn object that is an available member of the requested subgroup + for (var j = 0; j < m_SpawnObjects.Count; j++) + { + if (m_SpawnObjects[j].SubGroup == sgroup) + { + return j; + } + } + // failed to find any spawn entry of the requested subgroup + return -1; + } + + private void SeqResetTo(int sgroup) + { + // check the SequentialResetTo on the subgroup + // cant do resets on subgroup 0 + if (sgroup == 0) + { + return; + } + + // this will get the index of the first spawn entry in the subgroup + // it will have the subgroup timer settings + var spawnindex = GetCurrentSequentialSpawnIndex(sgroup); + + if (spawnindex >= 0) + { + // if it is greater than zero then initiate reset + var s = m_SpawnObjects[spawnindex]; + SequentialSpawn = s.SequentialResetTo; + + InitiateSequentialReset(sgroup); + + // clear the spawns + //RemoveSpawnObjects(); + ClearSubgroup(s.SubGroup); + + // and reset the kill count + KillCount = 0; + } + } + + private bool CheckForSequentialReset() + { + // check the SequentialResetTime on the subgroup + // cant do resets on subgroup 0 + if (SequentialSpawn == 0) + { + return false; + } + + // this will get the index of the first spawn entry in the subgroup + // it will have the subgroup timer settings + var spawnindex = GetCurrentSequentialSpawnIndex(SequentialSpawn); + + if (spawnindex >= 0) + { + // check the reset time on it + var s = m_SpawnObjects[spawnindex]; + // if it is greater than zero then resetting is possible + if (s.SequentialResetTime > 0) + { + // so check the reset timer + if (NextSeqReset <= TimeSpan.Zero) + { + // it has expired so time to reset + return true; + } + } + } + return false; + } + + private void InitiateSequentialReset(int sgroup) + { + // check the SequentialResetTime on the subgroup + // cant do resets on subgroup 0 + if (sgroup == 0) + { + return; + } + + // this will get the index of the first spawn entry in the subgroup + // it will have the subgroup timer settings + var spawnindex = GetCurrentSequentialSpawnIndex(sgroup); + + if (spawnindex >= 0) + { + // if it is greater than zero then initiate reset + var s = m_SpawnObjects[spawnindex]; + NextSeqReset = TimeSpan.FromMinutes(s.SequentialResetTime); + } + } + + public void ResetSequential() + { + // go back to the lowest level + if (SequentialSpawn >= 0) + { + SequentialSpawn = NextSequentialIndex(-1); + } + + // reset the nextspawn times + ResetNextSpawnTimes(); + + // and reset the kill count + KillCount = 0; + } + + public bool AdvanceSequential() + { + // check for a sequence hold + + if (HoldSequence) + { + return false; + } + + // check for triggering + if (!((m_proximityActivated || CanFreeSpawn) && TODInRange)) + { + return false; + } + + // if kills needed is greater than zero then check the killcount as well + var spawnindex = GetCurrentSequentialSpawnIndex(SequentialSpawn); + + var killsneeded = 0; + var subgroup = -1; + var clearedobjects = false; + + if (spawnindex >= 0) + { + var s = m_SpawnObjects[spawnindex]; + subgroup = s.SubGroup; + killsneeded = s.KillsNeeded; + } + + // advance the sequential spawn index if it is enabled and kills needed have been satisfied + if (SequentialSpawn >= 0 && (killsneeded == 0 || KillCount >= killsneeded)) + { + SequentialSpawn = NextSequentialIndex(SequentialSpawn); + + // set the sequential reset based on the current sequence state + // this will be checked in the spawner OnTick to determine whether to Reset the sequential state + InitiateSequentialReset(SequentialSpawn); + + // clear the spawns if there is a killcount on the level + if (killsneeded >= 0) + { + ClearSubgroup(subgroup); + clearedobjects = true; + } + + // and reset the kill count + KillCount = 0; + } + + // returning true will indicate that all spawns have been cleared and therefore a new spawn can be initiated in the same OnTick + return clearedobjects; + } + + private int killcount_held; + + public void OnTick() + { + TraceStart(8); + // start up the timer again for the next Ontick + DoTimer(); + + // reset the protection against runaway looping + ClearSpawnedThisTick = true; + + // if regional spawning is enabled, update the region in case new regions were added after the initialization pass + CheckRegionAssignment = true; + + // reset the killcount whenever a spawntick goes by in which it could have spawned, ie the spawner is full, or proximity triggered + // spawns were not activated. Note that killcount gets incremented within Defrag whenever a spawn is that had been generated is removed from the active list. + // Check the count before and then after the spawn passes. + // if the spawner is still refractory then dont do a reset of the killcount. + //int startcount = this.m_killcount; + var startcount = killcount_held; + if (!m_skipped) + { + killcount_held = m_killcount; + } + + // killcount will be updated in Defrag + Defrag(true); + + // remove any keyword tags that were made + // note, tags only last a single ontick except for WAIT type + ClearTags(false); + + if (!DisableGlobalAutoReset && startcount == m_killcount && !m_refractActivated && !m_skipped) + { + m_spawncheck--; + } + m_skipped = false; + + // allow for some slack in the killcount reset by resetting after a certain number of spawn ticks without kills pass + if (m_spawncheck <= 0) + { + m_killcount = 0; + m_spawncheck = KillReset; // wait for 1 spawn ticks to pass before resetting. This can be set to anything you like + } + + // check for smart spawning + if (SmartSpawning && IsFull && !HasActiveSectors && !HasDamagedOrDistantSpawns /*&& !HasHoldSmartSpawning */) + { + IsInactivated = true; + // for multiple sector spawning ranges use the sector timer, otherwise just rely on OnSectorActivate to detect sector activation + //if (!UseSectorActivate) + //DoSectorTimer(TimeSpan.FromSeconds(1)); + + SmartRemoveSpawnObjects(); + + } + + // dont process spawn ticks while inactivated if smart spawning is enabled + if (SmartSpawning && IsInactivated) + { + TraceEnd(8); + return; + } + + IsInactivated = false; + + // check to see if spawning is on hold due to a WAIT keyword + if (!OnHold) + { + // look for triggers that are not player activated. + if (m_ProximityRange == -1 && CanSpawn) + { + CheckTriggers(null, null, false); + } + + // check for proximity triggers without movement activation + if (m_ProximityRange >= 0 && CanSpawn) + { + // check all nearby players + foreach (var p in GetMobilesInRange(m_ProximityRange)) + { + if (ValidPlayerTrig(p)) + { + CheckTriggers(p, null, true); + } + } + } + + if (m_Group) + { + // check the seq reset time on the current subgroup + // if the reset time is greater than zero then check the timer + // if it has expired then reset the sequential subgroup + // only do this if it can actually spawn + if (CheckForSequentialReset()) + { + // it has expired so reset the sequential spawn level + SeqResetTo(SequentialSpawn); + + var triedtospawn = TryRespawn(); + + if (triedtospawn) + { + ClearGOTOTags(); + } + + // dont advance if the spawn isnt triggered after resetting + if (!triedtospawn) + { + HoldSequence = true; + } + } + else if (TotalSpawnedObjects <= 0) + { + + // advance the sequential spawn index if it is enabled + _ = AdvanceSequential(); + + //bool hadhold = HoldSequence; + + //HoldSequence = false; + + var triedtospawn = TryRespawn(); + + if (triedtospawn) + { + ClearGOTOTags(); + } + + //if (!triedtospawn) HoldSequence = hadhold; + } + } + else + { + + if (CheckForSequentialReset()) + { + // it has expired so reset the sequential spawn level + SeqResetTo(SequentialSpawn); + + // dont advance if the spawn isnt triggered after resetting + HoldSequence = true; + } + else + { + // advance the sequence before spawning + _ = AdvanceSequential(); + } + + // keep track of the hold flag before trying to spawn in case no spawn attempt is made + //bool hadhold = HoldSequence; + + // clear the hold flag to see if any of the spawned entries try to set it + //HoldSequence = false; + + // try to spawn. If spawning conditions such as triggering or TOD are not met, then it returns false + var triedtospawn = Spawn(false, 0); + + if (triedtospawn) + { + ClearGOTOTags(); + } + // this will maintain any sequential holds if spawning was suppressed due to triggering + + if (!FreeRun) + { + TriggerMob = null; + } + + } + + // remove any keyword tags that were made except for WAIT type + ClearTags(false); + + // and clear triggering flags + if (!OnHold && !FreeRun) + { + m_proximityActivated = false; + } + } + + if (FreeRun && SpawnOnTrigger && m_proximityActivated) + { + // if it is in free run and was triggered, then just keep spawning as though it was triggered immediately + NextSpawn = TimeSpan.Zero; + ResetNextSpawnTimes(); + } + + //this.m_ExternalTrigger = false; + // if it is out of the TOD range then delete the spawns + if (!TODInRange) + { + RemoveSpawnObjects(); + + ResetAllFlags(); + } + + TraceEnd(8); + } + + public bool ClearSpawnedThisTick + { + set + { + if (m_SpawnObjects == null || value == false) + { + return; + } + + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var sobj = m_SpawnObjects[i]; + if (sobj != null) + { + sobj.SpawnedThisTick = false; + } + } + } + } + + // select and spawn something + // return false if it cannot spawn, e.g. there is nothing to spawn or it is a triggerable spawner and has not been triggered + public bool Spawn(bool smartspawn, byte loops) + { + if (m_SpawnObjects?.Count > 0 && (m_proximityActivated || CanFreeSpawn) && TODInRange) + { + m_HoldSequence = false; + + // if the spawner is full then dont bother + if (IsFull) + { + ResetProximityActivated(); + return true; + } + + // Pick a spawn object to spawn + int SpawnIndex; + + // see if sequential spawning has been selected + SpawnIndex = SequentialSpawn >= 0 ? GetCurrentAvailableSequentialSpawnIndex(SequentialSpawn) : RandomAvailableSpawnIndex(); + + // no spawns are available so no point in continuing + if (SpawnIndex < 0) + { + ResetProximityActivated(); + return true; + } + + var sobj = m_SpawnObjects[SpawnIndex]; + var sgroup = sobj.SubGroup; + + // if this is part of a non-zero group, then spawn all of the group members as well + if (sgroup != 0) + { + _ = SpawnSubGroup(sgroup, smartspawn, loops); + } + else + { + // Found a valid spawn object so spawn it and see if it successful + if (Spawn(SpawnIndex, smartspawn, sobj.SpawnsPerTick, loops)) + { + if (!smartspawn) + { + RefreshNextSpawnTime(sobj); + } + } + } + + ResetProximityActivated(); + return true; + } + + ResetProximityActivated(); + return false; + } + + // spawn an individual entry by index up to count times + public bool Spawn(int index, bool smartspawn, int count, int packrange, Point3D packcoord, bool ignoreloopprotection, byte loops) + { + if (m_SpawnObjects == null || index >= m_SpawnObjects.Count) + { + return false; + } + + var didspawn = false; + + var so = m_SpawnObjects[index]; + + if (so == null) + { + return false; + } + + Defrag(false); + + // make sure you dont go over the individual entry maxcount + var somax = so.MaxCount; + var socnt = so.SpawnedObjects.Count; + var nspawn = so.SpawnsPerTick; + var scnt = SafeCurrentCount; + + for (var k = 0; k < nspawn && k + socnt < somax && k + scnt < MaxCount; k++) + { + if (packrange >= 0 && so.SubGroup > 0 && packcoord == Point3D.Zero) + { + packcoord = GetPackCoord(so.SubGroup); + } + if (Spawn(index, smartspawn, packrange, packcoord, ignoreloopprotection, loops)) + { + // if any of the attempts were successful then flag it as having spawned + didspawn = true; + } + } + + return didspawn; + } + + // spawn an individual entry by index up to count times + public bool Spawn(int index, bool smartspawn, int count, byte loops) => Spawn(index, smartspawn, count, false, loops); + + // spawn an individual entry by index up to count times + public bool Spawn(int index, bool smartspawn, int count, bool ignoreloopprotection, byte loops) => Spawn(index, smartspawn, count, -1, Point3D.Zero, ignoreloopprotection, loops); + + // spawn an individual entry by spawn object + public void Spawn(string SpawnObjectTypeName, bool smartspawn, int packrange, Point3D packcoord, byte loops) + { + if (m_SpawnObjects == null) + { + return; + } + + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + if (m_SpawnObjects[i].TypeName.ToUpper() == SpawnObjectTypeName.ToUpper()) + { + + if (Spawn(i, smartspawn, packrange, packcoord, loops)) + { + RefreshNextSpawnTime(m_SpawnObjects[i]); + } + break; + } + } + } + + // spawn an individual entry by index + public void Spawn(string SpawnObjectTypeName, bool smartspawn, byte loops) + { + Spawn(SpawnObjectTypeName, smartspawn, -1, Point3D.Zero, loops); + } + + // spawn an individual entry by index + public bool Spawn(int index, bool smartspawn, int packrange, Point3D packcoord, byte loops) => Spawn(index, smartspawn, packrange, packcoord, false, loops); + + // spawn an individual entry by index + public bool Spawn(int index, bool smartspawn, int packrange, Point3D packcoord, bool ignoreloopprotection, byte loops) + { + var map = Map; + + // Make sure everything is ok to spawn an object + if (map == null || + map == Map.Internal || + m_SpawnObjects == null || + m_SpawnObjects.Count == 0 || + index < 0 || + index >= m_SpawnObjects.Count + ) + { + return false; + } + + // Remove any spawns that don't belong to the spawner any more. + Defrag(false); + + // Get the spawn object at the required index + var TheSpawn = m_SpawnObjects[index]; + + // Check if the object retrieved is a valid SpawnObject + if (TheSpawn != null) + { + // dont allow an entry to be spawned more than once per tick + // this protects against runaway recursive looping + if (TheSpawn.SpawnedThisTick && !ignoreloopprotection) + { + return false; + } + + // check the nextspawn time to see if it is available + if (TheSpawn.NextSpawn > Core.Now) + { + return false; + } + + var CurrentCreatureMax = TheSpawn.MaxCount; + var CurrentCreatureCount = TheSpawn.SpawnedObjects.Count; + + // Check that the current object to be spawned has not reached its maximum allowed + // and make sure that the maximum spawner count has not been exceeded as well + if (CurrentCreatureCount >= CurrentCreatureMax || + TotalSpawnedObjects >= m_Count) + { + return false; + } + + // check for string substitions + var substitutedtypeName = BaseXmlSpawner.ApplySubstitution(this, this, TheSpawn.TypeName); + + // random positioning is the default + List spawnpositioning = null; + + // require valid surfaces by default + var requiresurface = true; + + // parse the # function specification for the entry + while (substitutedtypeName.StartsWith('#')) + { + var args = BaseXmlSpawner.ParseSemicolonArgs(substitutedtypeName, 2); + + if (args.Length > 0) + { + spawnpositioning ??= new List(); + // parse any comma args + var keyvalueargs = BaseXmlSpawner.ParseCommaArgs(args[0], 10); + + if (keyvalueargs.Length > 0) + { + + switch (keyvalueargs[0]) + { + case "#NOITEMID": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.NoItemID, TriggerMob, keyvalueargs)); + break; + } + case "#ITEMID": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.ItemID, TriggerMob, keyvalueargs)); + break; + } + case "#NOTILES": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.NoTiles, TriggerMob, keyvalueargs)); + break; + } + case "#TILES": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.Tiles, TriggerMob, keyvalueargs)); + break; + } + case "#WET": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.Wet, TriggerMob, keyvalueargs)); + break; + } + case "#XFILL": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.RowFill, TriggerMob, keyvalueargs)); + break; + } + case "#YFILL": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.ColFill, TriggerMob, keyvalueargs)); + break; + } + case "#EDGE": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.Perimeter, TriggerMob, keyvalueargs)); + break; + } + case "#PLAYER": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.Player, TriggerMob, keyvalueargs)); + break; + } + case "#WAYPOINT": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.Waypoint, TriggerMob, keyvalueargs)); + break; + } + case "#RELXY": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.RelXY, TriggerMob, keyvalueargs)); + break; + } + case "#DXY": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.DeltaLocation, TriggerMob, keyvalueargs)); + break; + } + case "#XY": + { + spawnpositioning.Add(new SpawnPositionInfo(SpawnPositionType.Location, TriggerMob, keyvalueargs)); + break; + } + case "#CONDITION": + { + // test the specified condition string + // syntax is #CONDITION,proptest + // reparse with only one arg after the comma, this allows property tests that use commas as well + var ckeyvalueargs = BaseXmlSpawner.ParseCommaArgs(args[0], 2); + if (ckeyvalueargs.Length > 1) + { + // dont spawn if it fails the test + if (!BaseXmlSpawner.CheckPropertyString(this, this, ckeyvalueargs[1], out var status)) + { + status_str = status; + return false; + } + } + else + { + status_str = $"invalid #CONDITION specification: {args[0]}"; + } + break; + } + default: + { + status_str = $"invalid # specification: {args[0]}"; + break; + } + } + } + } + + // get the rest of the spawn entry + substitutedtypeName = args.Length > 1 ? args[1].Trim() : string.Empty; + } + + if (substitutedtypeName.StartsWith("*")) + { + requiresurface = false; + substitutedtypeName = substitutedtypeName.TrimStart('*'); + } + + TheSpawn.RequireSurface = requiresurface; + + var typeName = BaseXmlSpawner.ParseObjectType(substitutedtypeName); + + if (BaseXmlSpawner.IsTypeOrItemKeyword(typeName)) + { + + var completedtypespawn = BaseXmlSpawner.SpawnTypeKeyword(this, TheSpawn, typeName, substitutedtypeName, + TriggerMob, Map, out var status_str, loops); + + if (status_str != null) + { + this.status_str = status_str; + } + + if (completedtypespawn) + { + // successfully spawned the keyword + // note that returning true means that Spawn will assume that it worked and will not try to respawn something else + // added the duration timer that begins on spawning + DoTimer2(m_Duration); + + InvalidateProperties(); + + return true; + } + + return false; + } + + // its a regular type descriptor so find out what it is + var type = AssemblyHandler.FindTypeByName(typeName); + + // dont try to spawn invalid types, or Mobile type spawns in containers + if (type != null && !(Parent != null && (type == typeof(Mobile) || type.IsSubclassOf(typeof(Mobile))))) + { + + var arglist = BaseXmlSpawner.ParseString(substitutedtypeName, 3, "/"); + + var o = CreateObject(type, arglist[0]); + + if (o == null) + { + status_str = $"invalid type specification: {arglist[0]}"; + return true; + } + try + { + if (o is Mobile mob) + { + // if this is in any container such as a pack the xyz values are invalid as map coords so dont spawn the mob + if (Parent is Container) + { + mob.Delete(); + + return true; + } + + // add the mobile to the spawned list + TheSpawn.SpawnedObjects.Add(mob); + + mob.Spawner = this; + + var loc = GetSpawnPosition(requiresurface, packrange, packcoord, spawnpositioning, mob); + + if (!smartspawn) + { + mob.OnBeforeSpawn(loc, map); + } + + mob.MoveToWorld(loc, map); + + if (mob is BaseCreature mobile) + { + mobile.RangeHome = m_HomeRange; + mobile.CurrentWayPoint = WayPoint; + + if (m_Team > 0) + { + mobile.Team = m_Team; + } + + // Check if this spawner uses absolute (from spawnER location) + // or relative (from spawnED location) as the mobiles home point + mobile.Home = HomeRangeIsRelative ? mobile.Location : Location; + } + + // if the object has an OnSpawned method, then invoke it + if (!smartspawn) + { + mob.OnAfterSpawn(); + } + + // apply the parsed arguments from the typestring using setcommand + // be sure to do this after setting map and location so that errors dont place the mob on the internal map + + _ = BaseXmlSpawner.ApplyObjectStringProperties(this, substitutedtypeName, mob, TriggerMob, this, out var status_str); + + if (status_str != null) + { + this.status_str = status_str; + } + + InvalidateProperties(); + + // added the duration timer that begins on spawning + DoTimer2(m_Duration); + + return true; + } + + if (o is Item item) + { + + BaseXmlSpawner.AddSpawnItem(this, TheSpawn, item, Location, map, TriggerMob, requiresurface, spawnpositioning, substitutedtypeName, smartspawn, out var status_str); + + if (status_str != null) + { + this.status_str = status_str; + } + + InvalidateProperties(); + + // added the duration timer that begins on spawning + DoTimer2(m_Duration); + + return true; + } + } + catch (Exception ex) { Console.WriteLine("When spawning {0}, {1}", o, ex); } + } + else + { + status_str = $"invalid type specification: {typeName}"; + return true; + } + } + return false; + } + + public bool SpawnSubGroup(int sgroup, byte loops) => SpawnSubGroup(sgroup, false, loops); + + public bool SpawnSubGroup(int sgroup, bool smartspawn, byte loops) => SpawnSubGroup(sgroup, false, false, loops); + + public bool SpawnSubGroup(int sgroup, bool smartspawn, bool ignoreloopprotection, byte loops) + { + if (m_SpawnObjects == null) + { + return false; + } + + if (sgroup >= 0) + { + var didspawn = false; + var packcoord = Point3D.Zero; + + for (var j = 0; j < m_SpawnObjects.Count; j++) + { + var so = m_SpawnObjects[j]; + + if (so?.SubGroup == sgroup) + { + // find the first subgroup spawn to determine the packspawning reference coordinates + if (so.PackRange >= 0 && packcoord == Point3D.Zero) + { + packcoord = GetPackCoord(sgroup); + } + + // get the SpawnsPerTick count and spawn up to that number + var success = Spawn(j, smartspawn, so.SpawnsPerTick, so.PackRange, packcoord, ignoreloopprotection, loops); + + if (success) + { + didspawn = true; + } + + if (success && !smartspawn) + { + RefreshNextSpawnTime(so); + } + } + } + + // success if any of the subgroup spawned + if (didspawn) + { + return true; + } + } + return false; + } + + public Point3D GetPackCoord(int sgroup) + { + for (var j = 0; j < m_SpawnObjects.Count; j++) + { + var so = m_SpawnObjects[j]; + + if (so?.SubGroup == sgroup && so.SpawnedObjects.Count > 0 && so.PackRange >= 0) + { + // if pack spawning is enabled for this subgroup, then get the + // the origin for pack spawning using the first existing pack spawn + // in the subgroup + + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + if (o is Item item) + { + return item.Location; + } + + if (o is Mobile mobile) + { + return mobile.Location; + } + } + } + } + + return Point3D.Zero; + } + + //used by the reset button in the gump + public void ResetAllFlags() + { + m_proximityActivated = false; + ExtTrigState = false; + m_durActivated = false; + m_refractActivated = false; + TriggerMob = null; + m_killcount = 0; + GumpState = null; + FreeRun = false; + } + + public bool BringHome + { + set + { + if (value) + { + BringToHome(); + } + } + } + + public void BringToHome() + { + if (m_SpawnObjects == null) + { + return; + } + + Defrag(false); + + foreach (var so in m_SpawnObjects) + { + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + + if (o is Mobile mobile) + { + mobile.Map = Map; + mobile.Location = new Point3D(Location); + } + else if (o is Item item) + { + item.MoveToWorld(Location, Map); + } + } + } + } + + public bool CheckRegionAssignment + { + get => false; + set + { + if (value) + { + // see if a region definition needs updating + if (m_Region == null && m_RegionName != null && RegionName != string.Empty) + { + RegionName = RegionName; + + if (SpawnRegion != null) + { + // clear the status if successful + status_str = null; + } + } + } + } + } + + public void Start() + { + if (!m_Running && m_SpawnObjects?.Count > 0) + { + m_Running = true; + DoTimer(); + } + } + + public void Stop() + { + if (m_Running) + { + // turn off all timers + m_Timer?.Stop(); + + m_DurTimer?.Stop(); + + m_RefractoryTimer?.Stop(); + + m_Running = false; + m_proximityActivated = false; + ExtTrigState = false; + TriggerMob = null; + } + } + + public void Reset() + { + Stop(); + // reset the protection against runaway looping + ClearSpawnedThisTick = true; + RemoveSpawnObjects(); + ClearTags(true); + ResetAllFlags(); + status_str = ""; + m_killcount = 0; + OnHold = false; + mostRecentSpawnPosition = Point3D.Zero; + spawnPositionWayTable = null; + // dont advance before the next spawn + HoldSequence = true; + IsInactivated = false; + ResetSequential(); + } + + public void Respawn() + { + _ = TryRespawn(); + } + + public bool TryRespawn() + { + inrespawn = true; + IsInactivated = false; + + // reset the protection against runaway looping + ClearSpawnedThisTick = true; + + // Delete all currently spawned objects + RemoveSpawnObjects(); + + // added the explicit start. Previously it relied on the automatic start that occurred when the spawnobject list was updated. + Start(); + + ResetNextSpawnTimes(); + + // Respawn all objects up to the spawners current maximum allowed + // note that by default, for proximity sensing, the spawner will only trigger once, but for respawns allow them all + var keepProximityActivated = m_proximityActivated; + + var triedtospawn = false; + + // attempt to spawn up to the MaxCount of the spawner + for (var x = 0; x < m_Count; x++) + { + triedtospawn = Spawn(false, 0); + + if (x < m_Count - 1 || OnHold) + { + m_proximityActivated = keepProximityActivated; + } + } + if (!FreeRun) + { + TriggerMob = null; + } + + ClearTags(true); + + inrespawn = false; + + return triedtospawn; + } + + // used to optimize smartspawning use of hasholdsmartspawning + public void SmartRespawn() + { + inrespawn = true; + IsInactivated = false; + + // reset the protection against runaway looping + ClearSpawnedThisTick = true; + + // Delete all currently spawned objects + SmartRemoveSpawnObjects(); + + // added the explicit start. Previously it relied on the automatic start that occurred when the spawnobject list was updated. + Start(); + + ResetNextSpawnTimes(); + + // Respawn all objects up to the spawners current maximum allowed + // note that by default, for proximity sensing, the spawner will only trigger once, but for respawns allow them all + var keepProximityActivated = m_proximityActivated; + + // attempt to spawn up to the MaxCount of the spawner + for (var x = 0; x < m_Count; x++) + { + _ = Spawn(true, 0); + + if (x < m_Count - 1 || OnHold) + { + m_proximityActivated = keepProximityActivated; + } + } + + if (!FreeRun) + { + TriggerMob = null; + } + + ClearTags(true); + + inrespawn = false; + } + + public void SortSpawns() + { + if (m_SpawnObjects == null) + { + return; + } + + // establish the entry order + var count = 0; + + foreach (var so in m_SpawnObjects) + { + so.EntryOrder = count++; + } + + m_SpawnObjects.Sort(new SubgroupSorter()); + } + + private class SubgroupSorter : IComparer + { + public int Compare(SpawnObject a, SpawnObject b) + { + if (a.SubGroup == b.SubGroup) + { + // use the entry order as the secondary sort factor + return a.EntryOrder - b.EntryOrder; + } + + return a.SubGroup - b.SubGroup; + } + } + + public static SpawnObject GetSpawnObject(XmlSpawner spawner, int sgroup) + { + if (spawner?.m_SpawnObjects == null) + { + return null; + } + + for (var i = 0; i < spawner.m_SpawnObjects.Count; i++) + { + // find the first entry with matching subgroup id + if (spawner.m_SpawnObjects[i].SubGroup == sgroup) + { + return spawner.m_SpawnObjects[i]; + } + } + return null; + } + + public static object GetSpawned(XmlSpawner spawner, int sgroup) + { + if (spawner?.m_SpawnObjects == null) + { + return null; + } + + for (var i = 0; i < spawner.m_SpawnObjects.Count; i++) + { + // find the first entry with matching subgroup id + if (spawner.m_SpawnObjects[i].SubGroup == sgroup) + { + // find the first spawned object in the entry + if (spawner.m_SpawnObjects[i].SpawnedObjects.Count > 0) + { + return spawner.m_SpawnObjects[i].SpawnedObjects[0]; + } + } + } + return null; + } + + public static List GetSpawnedList(XmlSpawner spawner, int sgroup) + { + var newlist = new List(); + + if (spawner?.m_SpawnObjects == null) + { + return null; + } + + for (var i = 0; i < spawner.m_SpawnObjects.Count; i++) + { + // find the first entry with matching subgroup id + if (spawner.m_SpawnObjects[i].SubGroup == sgroup) + { + // find the first spawned object in the entry + + if (spawner.m_SpawnObjects[i].SpawnedObjects.Count > 0) + { + for (var j = 0; j < spawner.m_SpawnObjects[i].SpawnedObjects.Count; j++) + { + newlist.Add(spawner.m_SpawnObjects[i].SpawnedObjects[j]); + } + } + } + } + return newlist; + } + + public bool HasSubGroups() + { + if (m_SpawnObjects == null) + { + return false; + } + + for (var j = 0; j < m_SpawnObjects.Count; j++) + { + if (m_SpawnObjects[j].SubGroup > 0) + { + return true; + } + } + + return false; + } + + private void ResetProximityActivated() + { + // dont reset triggering if free run mode has been selected + if (!FreeRun) + { + m_proximityActivated = false; + } + } + + public bool HasIndividualSpawnTimes() + { + + if (m_SpawnObjects?.Count > 0) + { + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var so = m_SpawnObjects[i]; + + if (so.MinDelay != -1 || so.MaxDelay != -1) + { + return true; + } + } + } + return false; + } + + private void ResetNextSpawnTimes() + { + + if (m_SpawnObjects?.Count > 0) + { + for (var i = 0; i < m_SpawnObjects.Count; i++) + { + var so = m_SpawnObjects[i]; + + so.NextSpawn = Core.Now; + } + } + } + + public static void RefreshNextSpawnTime(SpawnObject so) + { + if (so == null) + { + return; + } + + var mind = (int)(so.MinDelay * 60); + var maxd = (int)(so.MaxDelay * 60); + if (mind < 0 || maxd < 0) + { + so.NextSpawn = Core.Now; + } + else + { + + var delay = TimeSpan.FromSeconds(Utility.RandomMinMax(mind, maxd)); + + so.NextSpawn = Core.Now + delay; + } + + } + + public static bool IsValidMapLocation(int X, int Y, Map map) + { + if (map == null || map == Map.Internal) + { + return false; + } + + // check the location relative to the current map to make sure it is valid + if (X < 0 || X > map.Width || Y < 0 || Y > map.Height) + { + return false; + } + return true; + } + public static bool IsValidMapLocation(Point3D location, Map map) + { + if (map == null || map == Map.Internal) + { + return false; + } + + // check the location relative to the current map to make sure it is valid + if (location.X < 0 || location.X > map.Width || location.Y < 0 || location.Y > map.Height) + { + return false; + } + return true; + } + public static bool IsValidMapLocation(Point2D location, Map map) + { + if (map == null || map == Map.Internal) + { + return false; + } + + // check the location relative to the current map to make sure it is valid + if (location.X < 0 || location.X > map.Width || location.Y < 0 || location.Y > map.Height) + { + return false; + } + return true; + } + + private static WayPoint GetWaypoint(string waypointstr) + { + WayPoint waypoint = null; + + // try parsing the waypoint name to determine the waypoint. object syntax is "SERIAL,sernumber" or "waypointname" + if (!string.IsNullOrEmpty(waypointstr)) + { + var wayargs = BaseXmlSpawner.ParseString(waypointstr, 2, ","); + if (wayargs?.Length > 0) + { + // is this a SERIAL specification? + if (wayargs[0] == "SERIAL") + { + // look it up by serial + if (wayargs.Length > 1) + { + uint sernum; + try + { + sernum = (uint)Convert.ToUInt64(wayargs[1][2..], 16); + var e = World.FindEntity((Serial)sernum); + + if (e is WayPoint point) + { + waypoint = point; + } + } + catch { } + } + } + else + { + // just look it up by name + var wayitem = BaseXmlSpawner.FindItemByName(null, wayargs[0], "WayPoint"); + if (wayitem is WayPoint point) + { + waypoint = point; + } + } + } + } + + return waypoint; + } + + private static bool HasTileSurface(Map map, int X, int Y, int Z) + { + if (map == null) + { + return false; + } + + // go through the tiles and see if any are at the Z location + foreach (var staticTile in map.Tiles.GetStaticAndMultiTiles(X, Y)) + { + if (staticTile.Z + staticTile.Height == Z) + { + return true; + } + } + + return false; + } + + private static bool CheckHoldSmartSpawning(object o) + { + if (o == null) + { + return false; + } + + // try looking this up in the lookup table + holdSmartSpawningHash ??= new Dictionary(); + if (!holdSmartSpawningHash.TryGetValue(o.GetType(), out var prop)) + { + prop = o.GetType().GetProperty("HoldSmartSpawning"); + // check to make sure the HoldSmartSpawning property for this object has the right type + if (prop != null && (!prop.CanRead || prop.PropertyType != typeof(bool))) + { + prop = null; + } + + holdSmartSpawningHash[o.GetType()] = prop; + } + + if (prop != null) + { + try + { + return (bool)prop.GetValue(o, null); + } + catch { } + } + + return false; + } + + public bool HasHoldSmartSpawning + { + get + { + // go through the spawn lists + foreach (var so in m_SpawnObjects) + { + for (var x = 0; x < so.SpawnedObjects.Count; x++) + { + var o = so.SpawnedObjects[x]; + if (CheckHoldSmartSpawning(o)) + { + return true; + } + } + } + + return false; + } + } + + // if a non-null mob argument is passed, then check the canswim and cantwalk props to determine valid placement + public bool CanFit(int x, int y, int z, int height, bool checkBlocksFit, bool checkMobiles, bool requireSurface, Mobile mob) + { + var map = Map; + + if (DebugThis) + { + Console.WriteLine("CanFit mob {0}, map={1}", mob, map); + } + + if (map == null || map == Map.Internal) + { + return false; + } + + if (x < 0 || y < 0 || x >= map.Width || y >= map.Height) + { + return false; + } + + var hasSurface = false; + var checkmob = false; + var canswim = false; + var cantwalk = false; + + if (mob != null) + { + checkmob = true; + canswim = mob.CanSwim; + cantwalk = mob.CantWalk; + } + + if (DebugThis) + { + Console.WriteLine("fitting mob {0} checkmob={1} swim={2} walk={3}", mob, checkmob, canswim, cantwalk); + } + + var lt = map.Tiles.GetLandTile(x, y); + + bool surface; + var wet = false; + + map.GetAverageZ(x, y, out var lowZ, out var avgZ, out var topZ); + var landFlags = TileData.LandTable[lt.ID & TileData.MaxLandValue].Flags; + + if (DebugThis) + { + Console.WriteLine("landtile at {0},{1},{2} lowZ={3} avgZ={4} topZ={5}", x, y, z, lowZ, avgZ, topZ); + } + + var impassable = (landFlags & TileFlag.Impassable) != 0; + if (checkmob) + { + wet = (landFlags & TileFlag.Wet) != 0; + // dont allow wateronly creatures on land + if (cantwalk && !wet) + { + impassable = true; + } + + // allow water creatures on water + if (canswim && wet) + { + impassable = false; + } + } + + if (impassable && avgZ > z && z + height > lowZ) + { + return false; + } + + if (!impassable && z == avgZ && !lt.Ignored) + { + hasSurface = true; + } + + if (DebugThis) + { + Console.WriteLine("landtile at {0},{1},{2} wet={3} impassable={4} hassurface={5}", x, y, z, wet, impassable, hasSurface); + } + + foreach (var staticTile in map.Tiles.GetStaticAndMultiTiles(x, y)) + { + var id = TileData.ItemTable[staticTile.ID & TileData.MaxItemValue]; + surface = id.Surface; + impassable = id.Impassable; + if (checkmob) + { + wet = (id.Flags & TileFlag.Wet) != 0; + // dont allow wateronly creatures on land + if (cantwalk && !wet) + { + impassable = true; + } + + // allow water creatures on water + if (canswim && wet) + { + surface = true; + impassable = false; + } + } + + if ((surface || impassable) && staticTile.Z + id.CalcHeight > z && z + height > staticTile.Z) + { + return false; + } + + if (surface && !impassable && z == staticTile.Z + id.CalcHeight) + { + hasSurface = true; + } + } + if (DebugThis) + { + Console.WriteLine("statics hassurface={0}", hasSurface); + } + + foreach (var item in map.GetItemsAt(x, y)) + { + if (item.ItemID >= 0x4000) + { + continue; + } + + var id = item.ItemData; + surface = id.Surface; + impassable = id.Impassable; + if (checkmob) + { + wet = (id.Flags & TileFlag.Wet) != 0; + // dont allow wateronly creatures on land + if (cantwalk && !wet) + { + impassable = true; + } + + // allow water creatures on water + if (canswim && wet) + { + surface = true; + impassable = false; + } + } + + if ((surface || impassable || checkBlocksFit && item.BlocksFit) && item.Z + id.CalcHeight > z && z + height > item.Z) + { + return false; + } + + if (surface && !impassable && !item.Movable && z == item.Z + id.CalcHeight) + { + hasSurface = true; + } + } + + if (DebugThis) + { + Console.WriteLine("items hassurface={0}", hasSurface); + } + + if (checkMobiles) + { + foreach (var m in map.GetMobilesAt(x, y)) + { + if ((m.AccessLevel == AccessLevel.Player || !m.Hidden) && m.Z + 16 > z && z + height > m.Z) + { + return false; + } + } + } + + if (DebugThis) + { + Console.WriteLine("return requiresurface={0} hassurface={1}", requireSurface, hasSurface); + } + + return !requireSurface || hasSurface; + } + + public bool CanSpawnMobile(int x, int y, int z, Mobile mob) + { + if (DebugThis) + { + Console.WriteLine("CanSpawnMobile mob {0}", mob); + } + + return Region.Find(new Point3D(x, y, z), Map).AllowSpawn() && Map.CanFit(x, y, z, 16); + } + + public static bool HasRegionPoints(Region r) => r?.Area.Length > 0; + + public Rectangle2D SpawnerBounds => new(m_X, m_Y, m_Width + 1, m_Height + 1); + + private static void FindTileLocations( + ref List locations, + Map map, + int startx, + int starty, + int width, + int height, + List includetilelist, + List excludetilelist, + TileFlag tileflag, + bool checkitems, + int spawnerZ + ) + { + if (width < 0 || height < 0 || map == null) + { + return; + } + + locations ??= new List(); + + for (var x = startx; x <= startx + width; x++) + { + for (var y = starty; y <= starty + height; y++) + { + var allok = false; + var p = Point3D.Zero; + // go through all of the tiles at the location and find those that are in the allowed tiles list + var ltile = map.Tiles.GetLandTile(x, y); + var lflags = TileData.LandTable[ltile.ID & TileData.MaxLandValue].Flags; + + // check the land tile + bool includetile; + if (includetilelist?.Count > 0) + { + includetile = includetilelist.Contains(ltile.ID & TileData.MaxLandValue); + } + else + { + includetile = true; + } + + // non-excluded tiles must also be passable + bool excludetile; + if (excludetilelist?.Count > 0) + { + // also require the tile to be passable + excludetile = (lflags & TileFlag.Impassable) != 0 || excludetilelist.Contains(ltile.ID & TileData.MaxLandValue); + } + else + { + excludetile = false; + } + + if (includetile && !excludetile && (lflags & tileflag) == tileflag) + { + p = new Point3D(x, y, ltile.Z + ltile.Height); + allok = true; + } + + // check the static tiles + foreach (var stile in map.Tiles.GetStaticAndMultiTiles(x, y)) + { + var sflags = TileData.ItemTable[stile.ID & TileData.MaxItemValue].Flags; + + if (includetilelist?.Count > 0) + { + includetile = includetilelist.Contains(stile.ID & TileData.MaxItemValue); + } + else + { + includetile = true; + } + + // non-excluded tiles must also be passable + if (excludetilelist?.Count > 0) + { + excludetile = (sflags & TileFlag.Impassable) != 0 || excludetilelist.Contains(stile.ID & TileData.MaxItemValue); + } + else + { + excludetile = false; + } + + if (includetile && !excludetile && (sflags & tileflag) == tileflag) + { + //Console.WriteLine("found statictile {0}/{1} at {2},{3},{4}", stile.ID, stile.ID & 0x3fff, x, y, stile.Z + stile.Height); + if (p == Point3D.Zero) + { + p = new Point3D(x, y, stile.Z + stile.Height); + } + else if (!allok && p.Z - spawnerZ > Math.Abs(stile.Z - spawnerZ)) + { + p = new Point3D(x, y, stile.Z + stile.Height); + } + else if (Math.Abs(ltile.Z - spawnerZ) > Math.Abs(stile.Z - spawnerZ)) //maggiore distanza rispetto allo statico dallo spawner + { + p = new Point3D(x, y, stile.Z + stile.Height); + } + + allok = true; + //locations.Add(new Point3D(x, y, stile.Z + stile.Height)); + //break; + } + } + + if (checkitems) + { + // check the itemsid + foreach (var i in map.GetItemsAt(x, y)) + { + if (i.ItemData.Impassable) + { + excludetile = true; + } + + var iflags = TileData.ItemTable[i.ItemID & TileData.MaxItemValue].Flags; + if (includetilelist?.Count > 0) + { + includetile = includetilelist.Contains(i.ItemID & TileData.MaxItemValue); + } + else + { + includetile = true; + } + + if (excludetilelist?.Count > 0) + { + excludetile = excludetilelist.Contains(i.ItemID & TileData.MaxItemValue); + } + else + { + excludetile = false; + } + + if (includetile && !excludetile && (iflags & tileflag) == tileflag) + { + p = new Point3D(x, y, i.Z + i.ItemData.Height); + allok = true; + } + } + } + + if (allok && !excludetile) + { + locations.Add(p); + } + } + } + } + + private void FindRegionTileLocations(ref List locations, Region r, List includetilelist, List excludetilelist, TileFlag tileflag, bool checkitems, int spawnerZ) + { + if (r?.Area == null) + { + return; + } + + var count = r.Area.Length; + + locations ??= new List(); + + // calculate fields of all rectangles (for probability calculating) + for (var n = 0; n < count; n++) + { + var ra = r.Area[n]; + var sx = ra.Start.X; + var sy = ra.Start.Y; + var w = ra.Width; + var h = ra.Height; + + // find all of the valid tile locations in the area + FindTileLocations(ref locations, r.Map, sx, sy, w, h, includetilelist, excludetilelist, tileflag, checkitems, spawnerZ); + } + } + + public static Point2D GetRandomRegionPoint(Region r) + { + var count = r.Area.Length; + + var FieldArray = new int[count]; + var total = 0; + + // calculate fields of all rectangles (for probability calculating) + for (var i = 0; i < count; i++) + { + var ra = r.Area[i]; + total += FieldArray[i] = ra.Width * ra.Height; + } + + var sum = 0; + var rnd = 0; + if (total > 0) + { + rnd = Utility.Random(total); + } + + var x = 0; + var y = 0; + for (var i = 0; i < count; i++) + { + sum += FieldArray[i]; + if (sum > rnd) + { + var r3d = r.Area[i]; + if (r3d.Width >= 0) + { + x = r3d.Start.X + Utility.Random(r3d.Width); + } + + if (r3d.Height >= 0) + { + y = r3d.Start.Y + Utility.Random(r3d.Height); + } + + break; + } + } + + return new Point2D(x, y); + } + + public Point3D GetSpawnPosition(ISpawnable spawned, Map map) => GetSpawnPosition(true, spawned as Mobile); + + // used for getting non-mobile spawn positions + public Point3D GetSpawnPosition(bool requiresurface) => + // no pack spawning + GetSpawnPosition(requiresurface, -1, Point3D.Zero, null, null); + + // used for getting mobile spawn positions + public Point3D GetSpawnPosition(bool requiresurface, Mobile mob) => + // no pack spawning + GetSpawnPosition(requiresurface, -1, Point3D.Zero, null, mob); + + // used for getting non-mobile spawn positions + public Point3D GetSpawnPosition( + bool requiresurface, + int packrange, + Point3D packcoord, + List spawnpositioning + ) => + GetSpawnPosition(requiresurface, packrange, packcoord, spawnpositioning, null); + + public Point3D GetSpawnPosition(bool requiresurface, int packrange, Point3D packcoord, List spawnpositioning, Mobile mob) + { + var map = Map; + + if (map == null) + { + return Location; + } + + // random positioning by default + var positioning = SpawnPositionType.Random; + Mobile trigmob = null; + List includetilelist = null; + List excludetilelist = null; + var checkitems = false; + // restrictions on tile flags + var tileflag = TileFlag.None; + List locations = null; + + var fillinc = 1; + var positionrange = 0; + string prefix = null; + List WayList = null; + var xinc = 0; + var yinc = 0; + var zinc = 0; + if (spawnpositioning != null) + { + foreach (var s in spawnpositioning) + { + if (s == null) + { + continue; + } + + trigmob = s.trigMob; + var positionargs = s.positionArgs; + + // parse the possible args to the spawn position control keywords + switch (s.positionType) + { + case SpawnPositionType.Wet: + { + // syntax Wet + // find all of the wet tiles + tileflag |= TileFlag.Wet; + requiresurface = false; + break; + } + case SpawnPositionType.ItemID: + { + checkitems = true; + goto case SpawnPositionType.Tiles; + } + case SpawnPositionType.NoItemID: + + { + checkitems = true; + goto case SpawnPositionType.NoTiles; + } + case SpawnPositionType.Tiles: + { + // syntax Tiles,start[,end] + // get the tiles in the range + requiresurface = false; + var start = -1; + var end = -1; + if (positionargs?.Length > 1) + { + try + { + start = int.Parse(positionargs[1]); + } + catch { } + } + if (positionargs?.Length > 2) + { + try + { + end = int.Parse(positionargs[2]); + } + catch { } + } + includetilelist ??= new List(); + + // add the tiles to the list + if (start > -1 && end < 0) + { + includetilelist.Add(start); + } + else + if (start > -1 && end > -1) + { + for (var j = start; j <= end; j++) + { + includetilelist.Add(j); + } + } + break; + } + case SpawnPositionType.NoTiles: + { + // syntax Tiles,start[,end] + // get the tiles in the range + requiresurface = false; + var start = -1; + var end = -1; + if (positionargs?.Length > 1) + { + try + { + start = int.Parse(positionargs[1]); + } + catch { } + } + if (positionargs?.Length > 2) + { + try + { + end = int.Parse(positionargs[2]); + } + catch { } + } + excludetilelist ??= new List(); + + // add the tiles to the list + if (start > -1 && end < 0) + { + excludetilelist.Add(start); + } + else + if (start > -1 && end > -1) + { + for (var j = start; j <= end; j++) + { + excludetilelist.Add(j); + } + } + break; + } + case SpawnPositionType.RowFill: + case SpawnPositionType.ColFill: + case SpawnPositionType.Perimeter: + { + // syntax XFILL[,inc] + // syntax YFILL[,inc] + // syntax EDGE[,inc] + positioning = s.positionType; + if (positionargs?.Length > 1) + { + try + { + fillinc = int.Parse(positionargs[1]); + } + catch { } + } + break; + } + case SpawnPositionType.RelXY: + case SpawnPositionType.DeltaLocation: + case SpawnPositionType.Location: + { + // syntax RELXY,xinc,yinc[,zinc] + // syntax XY,x,y[,z] + // syntax DXY,dx,dy[,dz] + positioning = s.positionType; + if (positionargs?.Length > 2) + { + try + { + xinc = int.Parse(positionargs[1]); + yinc = int.Parse(positionargs[2]); + } + catch { } + } + if (positionargs?.Length > 3) + { + try + { + zinc = int.Parse(positionargs[3]); + } + catch { } + } + break; + } + case SpawnPositionType.Waypoint: + { + // syntax WAYPOINT,prefix[,range] + positioning = s.positionType; + if (positionargs?.Length > 1) + { + prefix = positionargs[1]; + } + + if (positionargs?.Length > 2) + { + try + { + positionrange = int.Parse(positionargs[2]); + } + catch { } + } + + // find a list of items that match the waypoint prefix + if (prefix != null) + { + // see if there is an existing hashtable for the waypoint lists + spawnPositionWayTable ??= new Dictionary>(); + + // no existing list so create a new one + if (!spawnPositionWayTable.TryGetValue(prefix, out WayList) || WayList == null) + { + WayList = new List(); + + foreach (var i in World.Items.Values) + { + if (i is WayPoint && !string.IsNullOrEmpty(i.Name) && i.Map == Map && i.Name == prefix) + { + // add it to the list of items + WayList.Add(i); + } + } + // add the new list to the local table + spawnPositionWayTable[prefix] = WayList; + } + } + break; + } + case SpawnPositionType.Player: + { + // syntax PLAYER[,range] + positioning = s.positionType; + if (positionargs?.Length > 1) + { + try + { + positionrange = int.Parse(positionargs[1]); + } + catch { } + } + break; + } + } + } + } + + // precalculate tile locations if they have been specified + if (includetilelist != null || excludetilelist != null || tileflag != TileFlag.None) + { + if (m_Region != null && HasRegionPoints(m_Region)) + { + FindRegionTileLocations(ref locations, m_Region, includetilelist, excludetilelist, tileflag, checkitems, Z); + } + else if (positioning == SpawnPositionType.Random) + { + FindTileLocations(ref locations, Map, m_X, m_Y, m_Width, m_Height, includetilelist, excludetilelist, tileflag, checkitems, Z); + } + } + + // Try 10 times to find a Spawnable location. + // trace profiling indicates that this is a major bottleneck + for (var i = 0; i < 10; i++) + { + var x = X; + var y = Y; + _ = Z; + + var defaultZ = Z; + if (packrange >= 0 && packcoord != Point3D.Zero) + { + defaultZ = packcoord.Z; + } + + if (packrange >= 0 && packcoord != Point3D.Zero) + { + // find a random coord relative to the packcoord + x = packcoord.X - packrange + Utility.Random(packrange * 2 + 1); + y = packcoord.Y - packrange + Utility.Random(packrange * 2 + 1); + } + else if (m_Region != null && HasRegionPoints(m_Region)) + { + // if region spawning is selected then use that to find an x,y loc instead of the spawn box + + if (includetilelist != null || excludetilelist != null || tileflag != TileFlag.None) + { + // use the precalculated tile locations + if (locations?.Count > 0) + { + var p = locations[Utility.Random(locations.Count)]; + x = p.X; + y = p.Y; + defaultZ = p.Z; + } + } + else + { + var p = GetRandomRegionPoint(m_Region); + x = p.X; + y = p.Y; + } + } + else + { + switch (positioning) + { + case SpawnPositionType.Random: + { + if (includetilelist != null || excludetilelist != null || tileflag != TileFlag.None) + { + + if (locations?.Count > 0) + { + var p = locations[Utility.Random(locations.Count)]; + x = p.X; + y = p.Y; + defaultZ = p.Z; + } + } + else + { + + if (m_Width > 0) + { + x = m_X + Utility.Random(m_Width + 1); + } + + if (m_Height > 0) + { + y = m_Y + Utility.Random(m_Height + 1); + } + } + break; + } + + case SpawnPositionType.RelXY: + { + x = mostRecentSpawnPosition.X + xinc; + y = mostRecentSpawnPosition.Y + yinc; + defaultZ = mostRecentSpawnPosition.Z + zinc; + break; + } + + case SpawnPositionType.DeltaLocation: + { + x = X + xinc; + y = Y + yinc; + defaultZ = Z + zinc; + break; + } + + case SpawnPositionType.Location: + { + x = xinc; + y = yinc; + defaultZ = zinc; + break; + } + + case SpawnPositionType.RowFill: + { + x = mostRecentSpawnPosition.X + fillinc; + y = mostRecentSpawnPosition.Y; + + if (x < m_X) + { + x = m_X; + } + + if (y < m_Y) + { + y = m_Y; + } + + if (x > m_X + m_Width) + { + x = m_X + (x - m_X - m_Width - 1); + y++; + } + + if (y > m_Y + m_Height) + { + y = m_Y; + } + + break; + } + + case SpawnPositionType.ColFill: + { + x = mostRecentSpawnPosition.X; + y = mostRecentSpawnPosition.Y + fillinc; + + if (x < m_X) + { + x = m_X; + } + + if (y < m_Y) + { + y = m_Y; + } + + if (y > m_Y + m_Height) + { + y = m_Y + (y - m_Y - m_Height - 1); + x++; + } + + if (x > m_X + m_Width) + { + x = m_X; + } + + break; + } + + case SpawnPositionType.Perimeter: + { + x = mostRecentSpawnPosition.X; + y = mostRecentSpawnPosition.Y; + + // if the point is not on the perimeter, reset it to the corner + if (x != m_X && x != m_X + m_Width && y != m_Y && y != m_Y + m_Height) + { + x = m_X; + y = m_Y; + } + + if (y == m_Y && x < m_X + m_Width) + { + x += fillinc; + } + else if (y == m_Y + m_Height && x > m_X) + { + x -= fillinc; + } + else if (x == m_X && y > m_Y) + { + y -= fillinc; + } + else if (x == m_X + m_Width && y < m_Y + m_Height) + { + y += fillinc; + } + + if (x > m_X + m_Width) + { + x = m_X + m_Width; + } + + if (y > m_Y + m_Height) + { + y = m_Y + m_Height; + } + + if (x < m_X) + { + x = m_X; + } + + if (y < m_Y) + { + y = m_Y; + } + + break; + } + + case SpawnPositionType.Player: + { + if (trigmob != null) + { + x = trigmob.Location.X; + y = trigmob.Location.Y; + if (positionrange > 0) + { + x += Utility.Random(positionrange * 2 + 1) - positionrange; + y += Utility.Random(positionrange * 2 + 1) - positionrange; + } + } + break; + } + + case SpawnPositionType.Waypoint: + { + // pick an item randomly from the waylist + if (WayList?.Count > 0) + { + var index = Utility.Random(WayList.Count); + var waypoint = WayList[index]; + if (waypoint != null) + { + x = waypoint.Location.X; + y = waypoint.Location.Y; + defaultZ = waypoint.Location.Z; + if (positionrange > 0) + { + x += Utility.Random(positionrange * 2 + 1) - positionrange; + y += Utility.Random(positionrange * 2 + 1) - positionrange; + } + } + } + + break; + } + } + + mostRecentSpawnPosition = new Point3D(x, y, defaultZ); + } + + // skip invalid points + if (x < 0 || y < 0 || x == 0 && y == 0) + { + continue; + } + + // try to find a valid spawn location using the z coord of the spawner + // relax the normal surface requirement for mobiles if the flag is set + var fit = requiresurface ? CanSpawnMobile(x, y, defaultZ, mob) : Map.CanFit(x, y, defaultZ, SpawnFitSize, true, false, false); + + // if that fails then try to find a valid z coord + if (fit) + { + return new Point3D(x, y, defaultZ); + } + + var z = Map.GetAverageZ(x, y); + + fit = requiresurface ? CanSpawnMobile(x, y, z, mob) : Map.CanFit(x, y, z, SpawnFitSize, true, false, false); + + if (fit) + { + return new Point3D(x, y, z); + } + } + + if (packrange >= 0 && packcoord != Point3D.Zero) + { + return packcoord; + } + + return Location; + } + + public int GetCreatureMax(int index) + { + Defrag(false); + return m_SpawnObjects?[index]?.MaxCount ?? 0; + } + + private static void DeleteFromList(List list) where T : IEntity + { + if (list == null) + { + return; + } + + var i = list.Count; + + while (--i >= 0) + { + if (i < list.Count) + { + try + { + list[i]?.Delete(); + } + catch + { } + } + } + + list.Clear(); + } + + private static void DeleteFromList(List listi, List listm) + { + DeleteFromList(listi); + DeleteFromList(listm); + } + + public void RemoveSpawnObjects() + { + if (m_SpawnObjects == null) + { + return; + } + + Defrag(false); + + ClearTags(true); + var deletelist = new List(); + foreach (var so in m_SpawnObjects) + { + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + + if (o is IEntity e) + { + deletelist.Add(e); + } + } + } + + DeleteFromList(deletelist); + + // Defrag again + Defrag(false); + } + + public void RemoveSpawnObjects(SpawnObject so) + { + if (so == null) + { + return; + } + + Defrag(false); + + var deletelist = new List(); + + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + + if (o is IEntity e) + { + deletelist.Add(e); + } + } + + DeleteFromList(deletelist); + + // Defrag again + Defrag(false); + } + + public void ClearSubgroup(int subgroup) + { + if (m_SpawnObjects == null) + { + return; + } + + Defrag(false); + + ClearTags(true); + var deletelist = new List(); + foreach (var so in m_SpawnObjects) + { + if (so.SubGroup != subgroup || !so.ClearOnAdvance) + { + continue; + } + + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + + if (o is IEntity e) + { + deletelist.Add(e); + } + } + } + + DeleteFromList(deletelist); + + // Defrag again + Defrag(false); + } + + // used to optimize smart spawning by removing all objects except those that have hold smartspawning + public void SmartRemoveSpawnObjects() + { + if (m_SpawnObjects == null) + { + return; + } + + Defrag(false); + + ClearTags(true); + var deletelist = new List(); + foreach (var so in m_SpawnObjects) + { + for (var i = 0; i < so.SpawnedObjects.Count; ++i) + { + var o = so.SpawnedObjects[i]; + + // new optimization for smart spawning to remove all objects except those with hold smartspawning enabled + if (CheckHoldSmartSpawning(o)) + { + continue; + } + + if (o is IEntity e) + { + deletelist.Add(e); + } + } + } + + DeleteFromList(deletelist); + + // Defrag again + Defrag(false); + } + + public void AddSpawnObject(string SpawnObjectName) + { + if (m_SpawnObjects == null) + { + return; + } + + Defrag(false); + + // Find the spawn object and increment its count by one + foreach (var so in m_SpawnObjects) + { + if (InsensitiveStringHelpers.Equals(so.TypeName, SpawnObjectName)) + { + // Add one to the total count + m_Count++; + + // Increment the max count for the current creature + so.ActualMaxCount++; + + //only spawn them immediately if the spawner is running + if (Running) + { + Spawn(SpawnObjectName, false, 0); + } + } + } + + InvalidateProperties(); + } + + public void DeleteSpawnObject(Mobile from, string SpawnObjectName) + { + var WasRunning = m_Running; + + try + { + // Stop spawning for a moment + Stop(); + + // Clean up any spawns marked as deleted + Defrag(false); + + // Keep a reference to the spawn object + SpawnObject TheSpawn = null; + + // Find the spawn object and increment its count by one + foreach (var so in m_SpawnObjects) + { + if (so.TypeName.ToUpper() == SpawnObjectName.ToUpper()) + { + // Set the spawn + TheSpawn = so; + break; + } + } + + // Was the spawn object found + if (TheSpawn != null) + { + var delete_this_entry = false; + + // Decrement the max count for the current creature + TheSpawn.ActualMaxCount--; + + // Make sure the spawn count does not go negative + if (TheSpawn.MaxCount < 0) + { + TheSpawn.MaxCount = 0; + delete_this_entry = true; + } + + if (!delete_this_entry) + { + // Subtract one to the total count + m_Count--; + } + + // Make sure the count does not go negative + if (m_Count < 0) + { + m_Count = 0; + + } + + var deletelist = new List(); + + // Remove any spawns over the count + while (TheSpawn.SpawnedObjects?.Count > 0 && TheSpawn.SpawnedObjects.Count > TheSpawn.MaxCount) + { + var o = TheSpawn.SpawnedObjects[0]; + + // Delete the object + if (o is IEntity e) + { + deletelist.Add(e); + } + + _ = TheSpawn.SpawnedObjects.Remove(o); + } + + DeleteFromList(deletelist); + + // Check if the spawn object should be removed + if (delete_this_entry) + { + _ = m_SpawnObjects.Remove(TheSpawn); + if (from != null) + { + var loc = GetWorldLocation(); + CommandLogging.WriteLine(from, $"{from.AccessLevel} {CommandLogging.Format(from)} removed from XmlSpawner {Serial} '{Name}' [{loc.X}, {loc.Y}] ({Map}) : {SpawnObjectName}"); + } + } + } + + InvalidateProperties(); + } + finally + { + if (WasRunning) + { + Start(); + } + } + } + + public void RemoveSpawnObject(SpawnObject so) + { + if (m_SpawnObjects.Contains(so)) + { + _ = m_SpawnObjects.Remove(so); + } + } + + public static object CreateObject(Type type, string itemtypestring) => CreateObject(type, itemtypestring, true); + + public static object CreateObject(Type type, string itemtypestring, bool requireConstructible) + { + // look for constructor arguments to be passed to it with the syntax type,arg1,arg2,.../ + var typewordargs = BaseXmlSpawner.ParseObjectArgs(itemtypestring); + + return CreateObject(type, typewordargs, requireConstructible); + } + + public static object CreateObject(Type type, string[] typewordargs, bool requireConstructible) + { + if (type == null) + { + return null; + } + + object o = null; + + var typearglen = 0; + if (typewordargs != null) + { + typearglen = typewordargs.Length; + } + + // ok, there are args in the typename, so we need to invoke the proper constructor + var ctors = type.GetConstructors(); + + // go through all the constructors for this type + for (var i = 0; i < ctors.Length; ++i) + { + var ctor = ctors[i]; + + // if requireConstructible is true, then allow either condition +#if RESTRICTConstructible + if (!(requireConstructible && Add.IsConstructible(ctor,requester))) + continue; +#else + if (!(requireConstructible && IsConstructible(ctor))) + { + continue; + } +#endif + + // check the parameter list of the constructor + var paramList = ctor.GetParameters(); + + // and compare with the argument list provided + if (typearglen == paramList.Length) + { + // this is a constructor that takes args and matches the number of args passed in to CreateObject + if (paramList.Length > 0) + { + object[] paramValues = null; + + try + { + paramValues = Add.ParseValues(paramList, typewordargs); + } + catch { } + + if (paramValues == null) + { + continue; + } + + // ok, have a match on args, so try to construct it + try + { + o = Activator.CreateInstance(type, paramValues); + } + catch { } + } + else + { + // zero argument constructor + try + { + o = Activator.CreateInstance(type); + } + catch { } + } + + // successfully constructed the object, otherwise try another matching constructor + if (o != null) + { + break; + } + } + } + + return o; + } + + private static void DoGlobalSectorTimer(TimeSpan delay) + { + m_GlobalSectorTimer?.Stop(); + + m_GlobalSectorTimer = new GlobalSectorTimer(delay); + + _ = m_GlobalSectorTimer.Start(); + } + + private class GlobalSectorTimer : Timer + { + + public GlobalSectorTimer(TimeSpan delay) : base(delay, delay) + { + } + + protected override void OnTick() + { + // check the sectors + + // check all active players + foreach (var state in TcpServer.Instances) + { + var m = state.Mobile; + + if (m != null && (m.AccessLevel <= SmartSpawnAccessLevel || !m.Hidden)) + { + // activate any spawner in the sector they are in + if (m.Map != null && m.Map != Map.Internal) + { + var s = m.Map.GetSector(m.Location); + + if (s != null && GlobalSectorTable[m.Map.MapID] != null) + { + + // = GlobalSectorTable[m.Map.MapID][s]; + if (GlobalSectorTable[m.Map.MapID].TryGetValue(s, out var spawnerlist) && spawnerlist != null) + { + foreach (var spawner in spawnerlist) + { + + if (spawner?.Deleted == false && spawner.Running && spawner.SmartSpawning && spawner.IsInactivated) + { + spawner.SmartRespawn(); + } + } + } + } + } + } + } + } + } + + public void DoSectorTimer(TimeSpan delay) + { + m_SectorTimer?.Stop(); + + m_SectorTimer = new SectorTimer(this, delay); + + _ = m_SectorTimer.Start(); + } + + private class SectorTimer : Timer + { + private readonly XmlSpawner m_Spawner; + + public SectorTimer(XmlSpawner spawner, TimeSpan delay) : base(delay, delay) => m_Spawner = spawner; + + protected override void OnTick() + { + // check the sectors + if (m_Spawner?.Deleted == false && m_Spawner.Running && m_Spawner.IsInactivated) + { + if (m_Spawner.SmartSpawning) + { + if (m_Spawner.HasActiveSectors) + { + Stop(); + + m_Spawner.SmartRespawn(); + } + } + else + { + Stop(); + + m_Spawner.IsInactivated = false; + } + } + else + { + Stop(); + + } + } + } + + private class WarnTimer2 : Timer + { + private readonly List m_List; + + private class WarnEntry2 + { + public Point3D m_Point; + public Map m_Map; + public string m_Name; + + public WarnEntry2(Point3D p, Map map, string name) + { + m_Point = p; + m_Map = map; + m_Name = name; + } + } + + public WarnTimer2() + : base(TimeSpan.FromSeconds(1.0)) + { + m_List = new List(); + _ = Start(); + } + + public void Add(Point3D p, Map map, string name) + { + m_List.Add(new WarnEntry2(p, map, name)); + } + + protected override void OnTick() + { + try + { + Console.WriteLine("Warning: {0} bad spawns detected, logged: 'badspawn.log'", m_List.Count); + + using var op = new StreamWriter("badspawn.log", true); + op.WriteLine("# Bad spawns : {0}", Core.Now); + op.WriteLine("# Format: X Y Z F Name"); + op.WriteLine(); + + foreach (var e in m_List) + { + op.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}", e.m_Point.X, e.m_Point.Y, e.m_Point.Z, e.m_Map, e.m_Name); + } + + op.WriteLine(); + op.WriteLine(); + } + catch + { } + } + } + + public void DoTimer() + { + if (!m_Running) + { + return; + } + + var minSeconds = (int)m_MinDelay.TotalSeconds; + var maxSeconds = (int)m_MaxDelay.TotalSeconds; + + var delay = TimeSpan.FromSeconds(Utility.RandomMinMax(minSeconds, maxSeconds)); + DoTimer(delay); + } + + public void DoTimer(TimeSpan delay) + { + if (!m_Running) + { + return; + } + + m_End = Core.Now + delay; + + m_Timer?.Stop(); + + m_Timer = new SpawnerTimer(this, delay); + _ = m_Timer.Start(); + } + + public void DoTimer2(TimeSpan delay) + { + m_DurEnd = Core.Now + delay; + if (m_Duration > TimeSpan.FromMinutes(0) || m_durActivated) + { + m_DurTimer?.Stop(); + + m_DurTimer = new InternalTimer(this, delay); + _ = m_DurTimer.Start(); + m_durActivated = true; + } + } + + public void DoTimer3(TimeSpan delay) + { + m_RefractEnd = Core.Now + delay; + m_refractActivated = true; + + m_RefractoryTimer?.Stop(); + + m_RefractoryTimer = new InternalTimer3(this, delay); + _ = m_RefractoryTimer.Start(); + } + + // added the duration timer that begins on spawning + private class InternalTimer : Timer + { + private readonly XmlSpawner m_spawner; + + public InternalTimer(XmlSpawner spawner, TimeSpan delay) : base(delay) => m_spawner = spawner; + + protected override void OnTick() + { + if (m_spawner?.Deleted == false) + { + m_spawner.RemoveSpawnObjects(); + m_spawner.m_durActivated = false; + } + + } + } + + private class SpawnerTimer : Timer + { + private readonly XmlSpawner m_Spawner; + + public SpawnerTimer(XmlSpawner spawner, TimeSpan delay) : base(delay) => m_Spawner = spawner; + + protected override void OnTick() + { + if (m_Spawner?.Deleted == false) + { + m_Spawner.OnTick(); + } + } + } + + // added the refractory timer that begins on proximity triggering + private class InternalTimer3 : Timer + { + private readonly XmlSpawner m_spawner; + + public InternalTimer3(XmlSpawner spawner, TimeSpan delay) : base(delay) => m_spawner = spawner; + + protected override void OnTick() + { + if (m_spawner?.Deleted == false) + { + // reenable triggering + m_spawner.m_refractActivated = false; + } + } + } + + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + + writer.Write(32); // version + // version 31 + writer.Write(DisableGlobalAutoReset); + // Version 30 + writer.Write(AllowNPCTrig); + + // Version 29 + if (m_SpawnObjects != null) + { + writer.Write(m_SpawnObjects.Count); + for (var i = 0; i < m_SpawnObjects.Count; ++i) + { + // Write the spawns per tick value + writer.Write(m_SpawnObjects[i].SpawnsPerTick); + } + } + else + { + // empty spawner + writer.Write(0); + } + + // Version 28 + if (m_SpawnObjects != null) + { + for (var i = 0; i < m_SpawnObjects.Count; ++i) + { + // Write the pack range value + writer.Write(m_SpawnObjects[i].PackRange); + } + } + + // Version 27 + if (m_SpawnObjects != null) + { + for (var i = 0; i < m_SpawnObjects.Count; ++i) + { + // Write the disable spawn flag + writer.Write(m_SpawnObjects[i].Disabled); + } + } + + // Version 26 + writer.Write(SpawnOnTrigger); + + // Version 24 + if (m_SpawnObjects != null) + { + for (var i = 0; i < m_SpawnObjects.Count; ++i) + { + var so = m_SpawnObjects[i]; + // Write the restrict kills flag + writer.Write(so.RestrictKillsToSubgroup); + // Write the clear on advance flag + writer.Write(so.ClearOnAdvance); + // Write the mindelay + writer.Write(so.MinDelay); + // Write the maxdelay + writer.Write(so.MaxDelay); + // write the next spawn time for the subgrop + writer.WriteDeltaTime(so.NextSpawn); + + } + } + + if (m_ShowBoundsItems?.Count > 0) + { + writer.Write(true); + writer.Write(m_ShowBoundsItems); + } + else + { + // empty showbounds item list + writer.Write(false); + } + + // Version 23 + writer.Write(IsInactivated); + writer.Write(m_SmartSpawning); + // Version 22 + writer.Write(SkillTrigger); + writer.Write((int)m_skill_that_triggered); + writer.Write(FreeRun); + writer.Write(TriggerMob); + // Version 21 + writer.Write(DespawnTime); + // Version 20 + if (m_SpawnObjects != null) + { + for (var i = 0; i < m_SpawnObjects.Count; ++i) + { + // Write the requiresurface flag + writer.Write(m_SpawnObjects[i].RequireSurface); + } + } + // Version 19 + writer.Write(ConfigFile); + writer.Write(m_OnHold); + writer.Write(m_HoldSequence); + // compute the number of tags to save + var tagcount = 0; + for (var i = 0; i < m_KeywordTagList.Count; i++) + { + // only save WAIT type keywords or other keywords that have the save flag set + if ((m_KeywordTagList[i].Flags & BaseXmlSpawner.KeywordFlags.Serialize) != 0) + { + tagcount++; + } + } + writer.Write(tagcount); + // and write them out + for (var i = 0; i < m_KeywordTagList.Count; i++) + { + if ((m_KeywordTagList[i].Flags & BaseXmlSpawner.KeywordFlags.Serialize) != 0) + { + m_KeywordTagList[i].Serialize(writer); + } + } + // Version 18 + writer.Write(AllowGhostTrig); + // Version 17 + // removed in version 25 + //writer.Write(m_TextEntryBook); + // Version 16 + writer.Write(SequentialSpawn); + // write out the remaining time until sequential reset + writer.Write(NextSeqReset); + // Write the spawn object list + if (m_SpawnObjects != null) + { + for (var i = 0; i < m_SpawnObjects.Count; ++i) + { + var so = m_SpawnObjects[i]; + // Write the subgroup and sequential reset time + writer.Write(so.SubGroup); + writer.Write(so.SequentialResetTime); + writer.Write(so.SequentialResetTo); + writer.Write(so.KillsNeeded); + } + } + writer.Write(m_RegionName); + + // Version 15 + writer.Write(ExternalTriggering); + writer.Write(ExtTrigState); + + // Version 14 + writer.Write(m_NoItemTriggerName); + + // Version 13 + writer.Write(GumpState); + + // Version 12 + var todtype = (int)TODMode; + writer.Write(todtype); + + // Version 11 + writer.Write(KillReset); + writer.Write(m_skipped); + writer.Write(m_spawncheck); + + // Version 10 + writer.Write(SetItem); + + // Version 9 + writer.Write(TriggerProbability); + + // Version 8 + writer.Write(MobTriggerProp); + writer.Write(MobTriggerName); + writer.Write(PlayerTriggerProp); + + // Version 7 + writer.Write(SpeechTrigger); + + // Version 6 + writer.Write(m_ItemTriggerName); + + // Version 5 + writer.Write(ProximityMsg); + writer.Write(m_ObjectPropertyItem); + writer.Write(m_ObjectPropertyName); + writer.Write(m_killcount); + + // Version 4 + writer.Write(m_ProximityRange); + writer.Write(ProximitySound); + writer.Write(m_proximityActivated); + writer.Write(m_durActivated); + writer.Write(m_refractActivated); + writer.Write(StackAmount); + writer.Write(TODStart); + writer.Write(TODEnd); + writer.Write(RefractMin); + writer.Write(RefractMax); + if (m_refractActivated) + { + writer.Write(m_RefractEnd - Core.Now); + } + + if (m_durActivated) + { + writer.Write(m_DurEnd - Core.Now); + } + + // Version 3 + writer.Write(m_ShowContainerStatic); + // Version 2 + writer.Write(m_Duration); + + // Version 1 + writer.Write(UniqueId); + writer.Write(HomeRangeIsRelative); + + // Version 0 + writer.Write(m_Name); + writer.Write(m_X); + writer.Write(m_Y); + writer.Write(m_Width); + writer.Write(m_Height); + writer.Write(WayPoint); + writer.Write(m_Group); + writer.Write(m_MinDelay); + writer.Write(m_MaxDelay); + writer.Write(m_Count); + writer.Write(m_Team); + writer.Write(m_HomeRange); + writer.Write(m_Running); + + if (m_Running) + { + writer.Write(m_End - Core.Now); + } + + // Write the spawn object list + var nso = 0; + if (m_SpawnObjects != null) + { + nso = m_SpawnObjects.Count; + } + + writer.Write(nso); + for (var i = 0; i < nso; ++i) + { + var so = m_SpawnObjects[i]; + + // Write the type and maximum count + writer.Write(so.TypeName); + writer.Write(so.ActualMaxCount); + + // Write the spawned object information + writer.Write(so.SpawnedObjects.Count); + for (var x = 0; x < so.SpawnedObjects.Count; ++x) + { + var o = so.SpawnedObjects[x]; + + if (o is Item item) + { + writer.Write(item); + } + else if (o is Mobile mobile) + { + writer.Write(mobile); + } + else + { + // if this is a keyword tag then add some more info + if (o is BaseXmlSpawner.KeywordTag tag) + { + writer.Write(-1 * tag.Serial - 2); + } + else + { + writer.Write(Serial.MinusOne); + } + } + } + } + } + + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + + var version = reader.ReadInt(); + var haveproximityrange = false; + var hasnewobjectinfo = false; + var tmpSpawnListSize = 0; + List tmpSubGroup = null; + List tmpSequentialResetTime = null; + List tmpSequentialResetTo = null; + List tmpKillsNeeded = null; + List tmpRequireSurface = null; + List tmpRestrictKillsToSubgroup = null; + List tmpClearOnAdvance = null; + List tmpMinDelay = null; + List tmpMaxDelay = null; + List tmpNextSpawn = null; + List tmpDisableSpawn = null; + List tmpPackRange = null; + List tmpSpawnsPer = null; + + switch (version) + { + case 32: + case 31: + { + DisableGlobalAutoReset = reader.ReadBool(); + goto case 30; + } + case 30: + { + AllowNPCTrig = reader.ReadBool(); + goto case 29; + } + case 29: + { + tmpSpawnListSize = reader.ReadInt(); + tmpSpawnsPer = new List(tmpSpawnListSize); + for (var i = 0; i < tmpSpawnListSize; ++i) + { + var spawnsper = reader.ReadInt(); + + tmpSpawnsPer.Add(spawnsper); + + } + goto case 28; + } + case 28: + { + tmpPackRange = new List(tmpSpawnListSize); + for (var i = 0; i < tmpSpawnListSize; ++i) + { + var packrange = reader.ReadInt(); + + tmpPackRange.Add(packrange); + + } + goto case 27; + } + case 27: + { + tmpDisableSpawn = new List(tmpSpawnListSize); + for (var i = 0; i < tmpSpawnListSize; ++i) + { + var disablespawn = reader.ReadBool(); + + tmpDisableSpawn.Add(disablespawn); + + } + goto case 26; + } + case 26: + { + SpawnOnTrigger = reader.ReadBool(); + + if (version < 32) + { + // Delete First & Last Modified + _ = reader.ReadDateTime(); + _ = reader.ReadDateTime(); + } + goto case 25; + } + case 25: + case 24: + { + tmpRestrictKillsToSubgroup = new List(tmpSpawnListSize); + tmpClearOnAdvance = new List(tmpSpawnListSize); + tmpMinDelay = new List(tmpSpawnListSize); + tmpMaxDelay = new List(tmpSpawnListSize); + tmpNextSpawn = new List(tmpSpawnListSize); + for (var i = 0; i < tmpSpawnListSize; ++i) + { + var restrictkills = reader.ReadBool(); + var clearadvance = reader.ReadBool(); + var mind = reader.ReadDouble(); + var maxd = reader.ReadDouble(); + var nextspawn = reader.ReadDeltaTime(); + + tmpRestrictKillsToSubgroup.Add(restrictkills); + tmpClearOnAdvance.Add(clearadvance); + tmpMinDelay.Add(mind); + tmpMaxDelay.Add(maxd); + tmpNextSpawn.Add(nextspawn); + } + + var hasitems = reader.ReadBool(); + + if (hasitems) + { + m_ShowBoundsItems = reader.ReadEntityList(); + } + goto case 23; + } + case 23: + { + IsInactivated = reader.ReadBool(); + SmartSpawning = reader.ReadBool(); + + goto case 22; + } + case 22: + { + SkillTrigger = reader.ReadString(); // note this will also register the skill + m_skill_that_triggered = (SkillName)reader.ReadInt(); + FreeRun = reader.ReadBool(); + TriggerMob = reader.ReadEntity(); + goto case 21; + } + case 21: + { + DespawnTime = reader.ReadTimeSpan(); + goto case 20; + } + case 20: + { + tmpRequireSurface = new List(tmpSpawnListSize); + for (var i = 0; i < tmpSpawnListSize; ++i) + { + var requiresurface = reader.ReadBool(); + tmpRequireSurface.Add(requiresurface); + } + goto case 19; + } + case 19: + { + ConfigFile = reader.ReadString(); + m_OnHold = reader.ReadBool(); + m_HoldSequence = reader.ReadBool(); + + if (version < 32) + { + // // Delete First & Last Modified By + // // Delete First & Last Modified By + _ = reader.ReadString(); + _ = reader.ReadString(); + } + + // deserialize the keyword tag list + var tagcount = reader.ReadInt(); + m_KeywordTagList = new List(tagcount); + for (var i = 0; i < tagcount; i++) + { + var tag = new BaseXmlSpawner.KeywordTag(null, this); + tag.Deserialize(reader); + } + goto case 18; + } + case 18: + { + AllowGhostTrig = reader.ReadBool(); + goto case 17; + } + case 17: + case 16: + { + hasnewobjectinfo = true; + SequentialSpawn = reader.ReadInt(); + var seqdelay = reader.ReadTimeSpan(); + m_SeqEnd = Core.Now + seqdelay; + + tmpSubGroup = new List(tmpSpawnListSize); + tmpSequentialResetTime = new List(tmpSpawnListSize); + tmpSequentialResetTo = new List(tmpSpawnListSize); + tmpKillsNeeded = new List(tmpSpawnListSize); + for (var i = 0; i < tmpSpawnListSize; ++i) + { + var subgroup = reader.ReadInt(); + var resettime = reader.ReadDouble(); + var resetto = reader.ReadInt(); + var killsneeded = reader.ReadInt(); + tmpSubGroup.Add(subgroup); + tmpSequentialResetTime.Add(resettime); + tmpSequentialResetTo.Add(resetto); + tmpKillsNeeded.Add(killsneeded); + } + m_RegionName = reader.ReadString(); + goto case 15; + } + case 15: + { + ExternalTriggering = reader.ReadBool(); + ExtTrigState = reader.ReadBool(); + goto case 14; + } + case 14: + { + m_NoItemTriggerName = reader.ReadString(); + goto case 13; + } + case 13: + { + GumpState = reader.ReadString(); + goto case 12; + } + case 12: + { + TODMode = (TODModeType)reader.ReadInt(); + goto case 11; + } + case 11: + { + KillReset = reader.ReadInt(); + m_skipped = reader.ReadBool(); + m_spawncheck = reader.ReadInt(); + goto case 10; + } + case 10: + { + SetItem = reader.ReadEntity(); + goto case 9; + } + case 9: + { + TriggerProbability = reader.ReadDouble(); + goto case 8; + } + case 8: + { + MobTriggerProp = reader.ReadString(); + MobTriggerName = reader.ReadString(); + PlayerTriggerProp = reader.ReadString(); + goto case 7; + } + case 7: + { + SpeechTrigger = reader.ReadString(); + goto case 6; + } + case 6: + { + m_ItemTriggerName = reader.ReadString(); + goto case 5; + } + case 5: + { + ProximityMsg = reader.ReadString(); + m_ObjectPropertyItem = reader.ReadEntity(); + m_ObjectPropertyName = reader.ReadString(); + m_killcount = reader.ReadInt(); + goto case 4; + } + case 4: + { + haveproximityrange = true; + m_ProximityRange = reader.ReadInt(); + ProximitySound = reader.ReadInt(); + m_proximityActivated = reader.ReadBool(); + m_durActivated = reader.ReadBool(); + m_refractActivated = reader.ReadBool(); + StackAmount = reader.ReadInt(); + TODStart = reader.ReadTimeSpan(); + TODEnd = reader.ReadTimeSpan(); + RefractMin = reader.ReadTimeSpan(); + RefractMax = reader.ReadTimeSpan(); + if (m_refractActivated) + { + var delay = reader.ReadTimeSpan(); + DoTimer3(delay); + } + if (m_durActivated) + { + var delay = reader.ReadTimeSpan(); + DoTimer2(delay); + } + goto case 3; + } + case 3: + { + m_ShowContainerStatic = reader.ReadEntity(); + goto case 2; + } + case 2: + { + m_Duration = reader.ReadTimeSpan(); + goto case 1; + } + case 1: + { + UniqueId = reader.ReadString(); + HomeRangeIsRelative = reader.ReadBool(); + goto case 0; + } + case 0: + { + m_Name = reader.ReadString(); + // backward compatibility with old name storage + if (!string.IsNullOrEmpty(m_Name)) + { + Name = m_Name; + } + + m_X = reader.ReadInt(); + m_Y = reader.ReadInt(); + m_Width = reader.ReadInt(); + m_Height = reader.ReadInt(); + //we HAVE to check if the area is even or if coordinates point to the original spawner, otherwise it's custom area! + if (m_Width == m_Height && m_Width % 2 == 0 && m_X + m_Width / 2 == X && m_Y + m_Height / 2 == Y) + { + m_SpawnRange = m_Width / 2; + } + else + { + m_SpawnRange = -1; + } + + if (!haveproximityrange) + { + m_ProximityRange = -1; + } + WayPoint = reader.ReadEntity(); + m_Group = reader.ReadBool(); + m_MinDelay = reader.ReadTimeSpan(); + m_MaxDelay = reader.ReadTimeSpan(); + m_Count = reader.ReadInt(); + m_Team = reader.ReadInt(); + m_HomeRange = reader.ReadInt(); + m_Running = reader.ReadBool(); + + if (m_Running) + { + var delay = reader.ReadTimeSpan(); + DoTimer(delay); + } + + // Read in the size of the spawn object list + var SpawnListSize = reader.ReadInt(); + m_SpawnObjects = new List(SpawnListSize); + for (var i = 0; i < SpawnListSize; ++i) + { + var TypeName = reader.ReadString(); + var TypeMaxCount = reader.ReadInt(); + + var TheSpawnObject = new SpawnObject(TypeName, TypeMaxCount); + + m_SpawnObjects.Add(TheSpawnObject); + + var typeName = BaseXmlSpawner.ParseObjectType(TypeName); + + if (typeName == null || AssemblyHandler.FindTypeByName(typeName) == null && + !BaseXmlSpawner.IsTypeOrItemKeyword(typeName) && !typeName.Contains('{') && !typeName.StartsWith("*") && !typeName.StartsWith("#")) + { + m_WarnTimer ??= new WarnTimer2(); + + m_WarnTimer.Add(Location, Map, TypeName); + + status_str = $"invalid type: {typeName}"; + } + + // Read in the number of spawns already + var SpawnedCount = reader.ReadInt(); + + TheSpawnObject.SpawnedObjects = new List(SpawnedCount); + + for (var x = 0; x < SpawnedCount; ++x) + { + var serial = reader.ReadInt(); + if (serial < -1) + { + // minusone is reserved for unknown types by default + // minustwo on is used for referencing keyword tags + var tagserial = -1 * (serial + 2); + // get the tag with that serial and add it + var t = BaseXmlSpawner.GetFromTagList(this, tagserial); + if (t != null) + { + TheSpawnObject.SpawnedObjects.Add(t); + } + } + else + { + var e = World.FindEntity((Serial)(uint)serial); + + if (e != null) + { + TheSpawnObject.SpawnedObjects.Add(e); + } + } + } + } + // now have to reintegrate the later version spawnobject information into the earlier version desered objects + if (hasnewobjectinfo && tmpSpawnListSize == SpawnListSize) + { + for (var i = 0; i < SpawnListSize; ++i) + { + var so = m_SpawnObjects[i]; + + so.SubGroup = tmpSubGroup[i]; + so.SequentialResetTime = tmpSequentialResetTime[i]; + so.SequentialResetTo = tmpSequentialResetTo[i]; + so.KillsNeeded = tmpKillsNeeded[i]; + if (version > 19) + { + so.RequireSurface = tmpRequireSurface[i]; + } + + var restrictkills = false; + var clearadvance = true; + double mind = -1; + double maxd = -1; + var nextspawn = DateTime.MinValue; + if (version > 23) + { + restrictkills = tmpRestrictKillsToSubgroup[i]; + clearadvance = tmpClearOnAdvance[i]; + mind = tmpMinDelay[i]; + maxd = tmpMaxDelay[i]; + nextspawn = tmpNextSpawn[i]; + } + so.RestrictKillsToSubgroup = restrictkills; + so.ClearOnAdvance = clearadvance; + so.MinDelay = mind; + so.MaxDelay = maxd; + so.NextSpawn = nextspawn; + + var disablespawn = false; + if (version > 26) + { + disablespawn = tmpDisableSpawn[i]; + } + so.Disabled = disablespawn; + + var packrange = -1; + if (version > 27) + { + packrange = tmpPackRange[i]; + } + so.PackRange = packrange; + + var spawnsper = 1; + if (version > 28) + { + spawnsper = tmpSpawnsPer[i]; + } + so.SpawnsPerTick = spawnsper; + + } + } + + break; + } + } + if (m_RegionName != null) + { + _ = Timer.DelayCall(delegate + { + if (!Deleted && m_RegionName != null) + { + RegionName = m_RegionName; + } + }); + } + } + + internal string GetSerializedObjectList() + { + var sb = new System.Text.StringBuilder(); + + foreach (var so in m_SpawnObjects) + { + if (sb.Length > 0) + { + _ = sb.Append(':'); // ':' Separates multiple object types + } + + _ = sb.AppendFormat("{0}={1}", so.TypeName, so.ActualMaxCount); // '=' separates object name from maximum amount + } + + return sb.ToString(); + } + + internal string GetSerializedObjectList2() + { + var sb = new System.Text.StringBuilder(); + + foreach (var so in m_SpawnObjects) + { + if (sb.Length > 0) + { + _ = sb.Append(":OBJ="); // Separates multiple object types + } + + _ = sb.Append( + $"{so.TypeName}:MX={so.ActualMaxCount}:SB={so.SubGroup}:RT={so.SequentialResetTime}:TO={so.SequentialResetTo}:KL={so.KillsNeeded}:RK={(so.RestrictKillsToSubgroup ? 1 : 0)}:CA={(so.ClearOnAdvance ? 1 : 0)}:DN={so.MinDelay}:DX={so.MaxDelay}:SP={so.SpawnsPerTick}:PR={so.PackRange}" + ); + } + + return sb.ToString(); + } + + public class SpawnObject + { + + // temporary variable used to calculate weighted spawn probabilities + public bool Available; + + public List SpawnedObjects; + public string[] PropertyArgs; + public double SequentialResetTime; + public int EntryOrder; // used for sorting + public bool RequireSurface = true; + public DateTime NextSpawn; + public bool SpawnedThisTick; + + // these are externally accessible to the SETONSPAWNENTRY keyword + public string TypeName { get; set; } + + public int MaxCount + { + get => Disabled ? 0 : ActualMaxCount; + set => ActualMaxCount = value; + } + public int ActualMaxCount { get; set; } + public int SubGroup { get; set; } + public int SpawnsPerTick { get; set; } = 1; + public int SequentialResetTo { get; set; } + public int KillsNeeded { get; set; } + public bool RestrictKillsToSubgroup { get; set; } + public bool ClearOnAdvance { get; set; } = true; + public double MinDelay { get; set; } = -1; + public double MaxDelay { get; set; } = -1; + public bool Disabled { get; set; } + public bool Ignore { get; set; } = false; + public int PackRange { get; set; } = -1; + + // command loggable constructor + public SpawnObject(Mobile from, XmlSpawner spawner, string name, int maxamount) + { + + if (from != null && spawner != null) + { + var found = false; + // go through the current spawner objects and see if this is a new entry + if (spawner.m_SpawnObjects != null) + { + for (var i = 0; i < spawner.m_SpawnObjects.Count; i++) + { + var s = spawner.m_SpawnObjects[i]; + if (s != null && s.TypeName == name) + { + found = true; + break; + } + } + } + + if (!found) + { + var loc = spawner.GetWorldLocation(); + CommandLogging.WriteLine(from, $"{from.AccessLevel} {CommandLogging.Format(from)} added to XmlSpawner {spawner.Serial} '{spawner.Name}' [{loc.X}, {loc.Y}] ({spawner.Map}) : {name}"); + } + } + + TypeName = name; + MaxCount = maxamount; + SubGroup = 0; + SequentialResetTime = 0; + SequentialResetTo = 0; + KillsNeeded = 0; + RestrictKillsToSubgroup = false; + ClearOnAdvance = true; + SpawnedObjects = new List(); + } + + public SpawnObject(string name, int maxamount) + { + TypeName = name; + MaxCount = maxamount; + SubGroup = 0; + SequentialResetTime = 0; + SequentialResetTo = 0; + KillsNeeded = 0; + RestrictKillsToSubgroup = false; + ClearOnAdvance = true; + SpawnedObjects = new List(); + } + + public SpawnObject(string name, int maxamount, int subgroup, double sequentialresettime, int sequentialresetto, int killsneeded, + bool restrictkills, bool clearadvance, double mindelay, double maxdelay, int spawnsper, int packrange) + { + TypeName = name; + MaxCount = maxamount; + SubGroup = subgroup; + SequentialResetTime = sequentialresettime; + SequentialResetTo = sequentialresetto; + KillsNeeded = killsneeded; + RestrictKillsToSubgroup = restrictkills; + ClearOnAdvance = clearadvance; + MinDelay = mindelay; + MaxDelay = maxdelay; + SpawnsPerTick = spawnsper; + PackRange = packrange; + SpawnedObjects = new List(); + } + + internal static string GetParm(string str, string separator) + { + // find the parm separator in the string + // then look for the termination at the ':' or end of string + // and return the stuff between + var arg = BaseXmlSpawner.SplitString(str, separator); + //should be 2 args + if (arg.Length > 1) + { + // look for the end of parm terminator (could also be eol) + var parm = arg[1].Split(':'); + if (parm.Length > 0) + { + return parm[0]; + } + } + return null; + } + + internal static SpawnObject[] LoadSpawnObjectsFromString(string ObjectList) + { + // Clear the spawn object list + var NewSpawnObjects = new List(); + + if (!string.IsNullOrEmpty(ObjectList)) + { + // Split the string based on the object separator first ':' + var SpawnObjectList = ObjectList.Split(':'); + + // Parse each item in the array + foreach (var s in SpawnObjectList) + { + // Split the single spawn object item by the max count '=' + var SpawnObjectDetails = s.Split('='); + + // Should be two entries + if (SpawnObjectDetails.Length == 2) + { + // Validate the information + + // Make sure the spawn object name part has a valid length + if (SpawnObjectDetails[0].Length > 0) + { + // Make sure the max count part has a valid length + if (SpawnObjectDetails[1].Length > 0) + { + var maxCount = 1; + + try + { + maxCount = int.Parse(SpawnObjectDetails[1]); + } + catch (Exception) + { // Something went wrong, leave the default amount } + } + + // Create the spawn object and store it in the array list + var so = new SpawnObject(SpawnObjectDetails[0], maxCount); + NewSpawnObjects.Add(so); + } + } + } + } + } + + return NewSpawnObjects.ToArray(); + } + + internal static SpawnObject[] LoadSpawnObjectsFromString2(string ObjectList) + { + // Clear the spawn object list + var NewSpawnObjects = new List(); + + // spawn object definitions will take the form typestring:MX=int:SB=int:RT=double:TO=int:KL=int + // or typestring:MX=int:SB=int:RT=double:TO=int:KL=int:OBJ=typestring... + if (!string.IsNullOrEmpty(ObjectList)) + { + var SpawnObjectList = BaseXmlSpawner.SplitString(ObjectList, ":OBJ="); + + // Parse each item in the array + foreach (var s in SpawnObjectList) + { + // at this point each spawn string will take the form typestring:MX=int:SB=int:RT=double:TO=int:KL=int + // Split the single spawn object item by the max count to get the typename and the remaining parms + var SpawnObjectDetails = BaseXmlSpawner.SplitString(s, ":MX="); + + // Should be two entries + if (SpawnObjectDetails.Length == 2) + { + // Validate the information + + // Make sure the spawn object name part has a valid length + if (SpawnObjectDetails[0].Length > 0) + { + // Make sure the parm part has a valid length + if (SpawnObjectDetails[1].Length > 0) + { + // now parse out the parms + // MaxCount + var parmstr = GetParm(s, ":MX="); + var maxCount = 1; + try { maxCount = int.Parse(parmstr); } + catch { } + + // SubGroup + parmstr = GetParm(s, ":SB="); + + var subGroup = 0; + try { subGroup = int.Parse(parmstr); } + catch { } + + // SequentialSpawnResetTime + parmstr = GetParm(s, ":RT="); + double resetTime = 0; + try { resetTime = double.Parse(parmstr); } + catch { } + + // SequentialSpawnResetTo + parmstr = GetParm(s, ":TO="); + var resetTo = 0; + try { resetTo = int.Parse(parmstr); } + catch { } + + // KillsNeeded + parmstr = GetParm(s, ":KL="); + var killsNeeded = 0; + try { killsNeeded = int.Parse(parmstr); } + catch { } + + // RestrictKills + parmstr = GetParm(s, ":RK="); + var restrictKills = false; + if (parmstr != null) + { + try { restrictKills = int.Parse(parmstr) == 1; } + catch { } + } + + // ClearOnAdvance + parmstr = GetParm(s, ":CA="); + // if kills needed is zero, then set CA to false by default. This maintains consistency with the + // previous default behavior for old spawn specs that haven't specified CA + var clearAdvance = killsNeeded != 0; + if (parmstr != null) + { + try { clearAdvance = int.Parse(parmstr) == 1; } + catch { } + } + + // MinDelay + parmstr = GetParm(s, ":DN="); + double minD = -1; + try { minD = double.Parse(parmstr); } + catch { } + + // MaxDelay + parmstr = GetParm(s, ":DX="); + double maxD = -1; + try { maxD = double.Parse(parmstr); } + catch { } + + // SpawnsPerTick + parmstr = GetParm(s, ":SP="); + var spawnsPer = 1; + try { spawnsPer = int.Parse(parmstr); } + catch { } + + // PackRange + parmstr = GetParm(s, ":PR="); + var packRange = -1; + try { packRange = int.Parse(parmstr); } + catch { } + + // Create the spawn object and store it in the array list + var so = new SpawnObject(SpawnObjectDetails[0], maxCount, subGroup, resetTime, resetTo, killsNeeded, + restrictKills, clearAdvance, minD, maxD, spawnsPer, packRange); + + NewSpawnObjects.Add(so); + } + } + } + } + } + + return NewSpawnObjects.ToArray(); + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlSpawnerGumps.cs b/Projects/UOContent/Engines/XMLSpawner/XmlSpawnerGumps.cs new file mode 100644 index 000000000..9d6605b87 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlSpawnerGumps.cs @@ -0,0 +1,1322 @@ +#define NEWPROPSGUMP +#define BOOKTEXTENTRY +using Server.Commands; +using Server.Gumps; +using Server.Items; +using Server.Network; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Server.Mobiles; + +public class TextEntryGump : Gump +{ + private readonly XmlSpawner m_Spawner; + private readonly int m_index; + private readonly XmlSpawnerGump m_SpawnerGump; + + public TextEntryGump(XmlSpawner spawner, XmlSpawnerGump spawnergump, int index, int X, int Y) + : base(X, Y) + { + if (spawner == null || spawner.Deleted) + { + return; + } + + m_Spawner = spawner; + m_index = index; + m_SpawnerGump = spawnergump; + + AddPage(0); + + AddBackground(20, 0, 220, 354, 5054); + AddAlphaRegion(20, 0, 220, 354); + AddImageTiled(23, 5, 214, 270, 0x52); + AddImageTiled(24, 6, 213, 261, 0xBBC); + + var label = $"{spawner.Name} entry {index}"; + AddLabel(28, 10, 0x384, label); + + // OK button + AddButton(25, 325, 0xFB7, 0xFB9, 1); + // Close button + AddButton(205, 325, 0xFB1, 0xFB3, 0); + // Edit button + AddButton(100, 325, 0xEF, 0xEE, 2); + string str = null; + if (index < m_Spawner.SpawnObjects.Length) + { + str = m_Spawner.SpawnObjects[index].TypeName; + } + // main text entry area + AddTextEntry(35, 30, 200, 251, 0, 0, str); + + // editing text entry areas + // background for text entry area + AddImageTiled(23, 275, 214, 23, 0x52); + AddImageTiled(24, 276, 213, 21, 0xBBC); + AddImageTiled(23, 300, 214, 23, 0x52); + AddImageTiled(24, 301, 213, 21, 0xBBC); + + AddTextEntry(35, 275, 200, 21, 0, 1, null); + AddTextEntry(35, 300, 200, 21, 0, 2, null); + } + + public override void OnResponse(NetState state, RelayInfo info) + { + if (info == null || state?.Mobile == null) + { + return; + } + + if (m_Spawner == null || m_Spawner.Deleted) + { + return; + } + + var update_entry = false; + var edit_entry = false; + + switch (info.ButtonID) + { + case 0: // Close + { + break; + } + case 1: // Okay + { + update_entry = true; + break; + } + case 2: // Edit + { + edit_entry = true; + break; + } + default: + { + update_entry = true; + break; + } + } + if (edit_entry) + { + // get the old text + var entry = info.GetTextEntry(1); + var oldtext = entry.Text; + // get the new text + entry = info.GetTextEntry(2); + var newtext = entry.Text; + // make the substitution + entry = info.GetTextEntry(0); + var origtext = entry.Text; + if (origtext != null && oldtext != null && newtext != null) + { + try + { + var firstindex = origtext.IndexOf(oldtext); + if (firstindex >= 0) + { + + + var secondindex = firstindex + oldtext.Length; + + var lastindex = origtext.Length - 1; + + string editedtext; + if (firstindex > 0) + { + editedtext = origtext.Substring(0, firstindex) + newtext + origtext.Substring(secondindex, lastindex - secondindex + 1); + } + else + { + editedtext = newtext + origtext.Substring(secondindex, lastindex - secondindex + 1); + } + + if (m_index < m_Spawner.SpawnObjects.Length) + { + m_Spawner.SpawnObjects[m_index].TypeName = editedtext; + } + else + { + // Update the creature list + m_Spawner.SpawnObjects = m_SpawnerGump.CreateArray(info, state.Mobile); + } + } + } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + + } + // open a new text entry gump + state.Mobile.SendGump(new TextEntryGump(m_Spawner, m_SpawnerGump, m_index, X, Y)); + return; + } + if (update_entry) + { + var entry = info.GetTextEntry(0); + if (m_index < m_Spawner.SpawnObjects.Length) + { + m_Spawner.SpawnObjects[m_index].TypeName = entry.Text; + } + else + { + // Update the creature list + m_Spawner.SpawnObjects = m_SpawnerGump.CreateArray(info, state.Mobile); + } + } + + // open a new spawner gump + state.Mobile.SendGump(new XmlSpawnerGump(m_Spawner, X, Y, m_SpawnerGump.m_ShowGump, m_SpawnerGump.xoffset, m_SpawnerGump.page)); + } +} + +public class XmlSpawnerGump : Gump +{ + private static int nclicks; + public XmlSpawner m_Spawner; + public const int MaxSpawnEntries = 60; + private const int MaxEntriesPerPage = 15; + public int m_ShowGump; + public int xoffset; + public int initial_maxcount; + public int page; + public ReplacementEntry Rentry; + + public class ReplacementEntry + { + public string Typename; + public int Index; + public int Color; + } + + public XmlSpawnerGump(XmlSpawner spawner, int X, int Y, int extension, int textextension, int newpage) + : this(spawner, X, Y, extension, textextension, newpage, null) + { + } + + public XmlSpawnerGump(XmlSpawner spawner, int X, int Y, int extension, int textextension, int newpage, ReplacementEntry rentry) + : base(X, Y) + { + if (spawner == null || spawner.Deleted) + { + return; + } + + m_Spawner = spawner; + spawner.SpawnerGump = this; + xoffset = textextension; + initial_maxcount = spawner.MaxCount; + page = newpage; + Rentry = rentry; + + AddPage(0); + + // automatically change the gump depending on whether sequential spawning and/or subgroups are activated + + if (spawner.SequentialSpawn >= 0 || spawner.HasSubGroups() || spawner.HasIndividualSpawnTimes()) + { + // show the fully extended gump with subgroups and reset timer info + m_ShowGump = 2; + } + + if (extension > 0) + { + m_ShowGump = extension; + } + if (extension < 0) + { + m_ShowGump = 0; + } + + // if the expanded gump toggle has been activated then override the auto settings. + if (m_ShowGump > 1) + { + AddBackground(0, 0, 670 + xoffset + 30, 474, 5054); + AddAlphaRegion(0, 0, 670 + xoffset + 30, 474); + } + else + if (m_ShowGump > 0) + { + AddBackground(0, 0, 335 + xoffset, 474, 5054); + AddAlphaRegion(0, 0, 335 + xoffset, 474); + } + else + { + AddBackground(0, 0, 305 + xoffset, 474, 5054); + AddAlphaRegion(0, 0, 305 + xoffset, 474); + } + + // spawner name area + AddImageTiled(3, 5, 227, 23, 0x52); + AddImageTiled(4, 6, 225, 21, 0xBBC); + AddTextEntry(6, 5, 222, 21, 0, 999, spawner.Name); // changed from color 50 + + AddButton(5, 450, 0xFAE, 0xFAF, 4); + AddLabel(38, 450, 0x384, "Goto"); + + //AddButton(5, 428, 0xFB7, 0xFB9, 1, GumpButtonType.Reply, 0); + AddButton(5, 428, 0xFAE, 0xFAF, 1); + AddLabel(38, 428, 0x384, "Help"); + + AddButton(71, 428, 0xFB4, 0xFB6, 2); + AddLabel(104, 428, 0x384, "Bring Home"); + AddButton(71, 450, 0xFA8, 0xFAA, 3); + AddLabel(104, 450, 0x384, "Respawn"); + + // Props button + AddButton(168, 428, 0xFAB, 0xFAD, 9999); + AddLabel(201, 428, 0x384, "Props"); + + // Sort button + AddButton(168, 450, 0xFAB, 0xFAD, 702); + AddLabel(201, 450, 0x384, "Sort"); + + // Reset button + AddButton(71, 406, 0xFA2, 0xFA3, 701); + AddLabel(104, 406, 0x384, "Reset"); + + // Refresh button + AddButton(168, 406, 0xFBD, 0xFBE, 9998); + AddLabel(201, 406, 0x384, "Refresh"); + + AddButton(280, 395, m_Spawner.DisableGlobalAutoReset ? 0xD3 : 0xD2, + m_Spawner.DisableGlobalAutoReset ? 0xD2 : 0xD3, 9997); + AddLabel(263, 410, m_Spawner.DisableGlobalAutoReset ? 68 : 33, "Disable"); + AddLabel(247, 424, m_Spawner.DisableGlobalAutoReset ? 68 : 33, "TickReset"); + + // add run status display + if (m_Spawner.Running) + { + AddButton(5, 399, 0x2A4E, 0x2A3A, 700); + AddLabel(38, 406, 0x384, "On"); + } + else + { + AddButton(5, 399, 0x2A62, 0x2A3A, 700); + AddLabel(38, 406, 0x384, "Off"); + } + + // Add sequential spawn state + if (m_Spawner.SequentialSpawn >= 0) + { + AddLabel(15, 365, 33, $"{m_Spawner.SequentialSpawn}"); + } + + // Add Current / Max count labels + AddLabel(231 + xoffset, 9, 68, "Count"); + AddLabel(270 + xoffset, 9, 33, "Max"); + + if (m_ShowGump > 0) + { + // Add subgroup label + AddLabel(334 + xoffset, 9, 68, "Sub"); + } + if (m_ShowGump > 1) + { + // Add entry field labels + AddLabel(303 + xoffset, 9, 68, "Per"); + AddLabel(329 + xoffset + 30, 9, 68, "Reset"); + AddLabel(368 + xoffset + 30, 9, 68, "To"); + AddLabel(392 + xoffset + 30, 9, 68, "Kills"); + AddLabel(432 + xoffset + 30, 9, 68, "MinD"); + AddLabel(472 + xoffset + 30, 9, 68, "MaxD"); + AddLabel(515 + xoffset + 30, 9, 68, "Rng"); + AddLabel(545 + xoffset + 30, 9, 68, "RK"); + AddLabel(565 + xoffset + 30, 9, 68, "Clr"); + AddLabel(590 + xoffset + 30, 9, 68, "NextSpawn"); + } + + // add area for spawner max + AddLabel(180 + xoffset, 365, 50, "Spawner"); + AddImageTiled(267 + xoffset, 365, 35, 23, 0x52); + AddImageTiled(268 + xoffset, 365, 32, 21, 0xBBC); + AddTextEntry(273 + xoffset, 365, 33, 33, 33, 300, m_Spawner.MaxCount.ToString()); + + // add area for spawner count + AddImageTiled(231 + xoffset, 365, 35, 23, 0x52); + AddImageTiled(232 + xoffset, 365, 32, 21, 0xBBC); + AddLabel(233 + xoffset, 365, 68, m_Spawner.CurrentCount.ToString()); + + // add the status string + AddTextEntry(38, 384, 235, 33, 33, 900, m_Spawner.status_str); + // add the page buttons + for (var i = 0; i < MaxSpawnEntries / MaxEntriesPerPage; i++) + { + //AddButton(38+i*30, 365, 2206, 2206, 0, GumpButtonType.Page, 1+i); + AddButton(38 + i * 25, 365, 0x8B1 + i, 0x8B1 + i, 4000 + i); + } + + // add gump extension button + if (m_ShowGump > 1) + { + AddButton(645 + xoffset + 30, 450, 0x15E3, 0x15E7, 200); + } + else + if (m_ShowGump > 0) + { + AddButton(315 + xoffset, 450, 0x15E1, 0x15E5, 200); + } + else + { + AddButton(285 + xoffset, 450, 0x15E1, 0x15E5, 200); + } + + // add the textentry extender button + if (xoffset > 0) + { + AddButton(160, 365, 0x15E3, 0x15E7, 201); + } + else + { + AddButton(160, 365, 0x15E1, 0x15E5, 201); + } + + + for (var i = 0; i < MaxSpawnEntries; i++) + { + if (page != i / MaxEntriesPerPage) + { + continue; + } + + var str = string.Empty; + var texthue = 0; + var background = 0xBBC; + + if (i % MaxEntriesPerPage == 0) + { + //AddPage(page+1); + // add highlighted page button + AddImageTiled(35 + page * 25, 363, 25, 25, 0xBBC); + AddImage(38 + page * 25, 365, 0x8B1 + page); + } + + if (i < m_Spawner.SpawnObjects.Length) + { + // disable button + + if (m_Spawner.SpawnObjects[i].Disabled) + { + // change the background for the spawn text entry if disabled + background = 0x23F4; + AddButton(2, 22 * (i % MaxEntriesPerPage) + 34, 0x82C, 0x82C, 6000 + i); + } + else + { + AddButton(2, 22 * (i % MaxEntriesPerPage) + 36, 0x837, 0x837, 6000 + i); + } + } + + var hasreplacement = false; + + // check for replacement entries + if (Rentry != null && Rentry.Index == i) + { + hasreplacement = true; + str = Rentry.Typename; + background = Rentry.Color; + // replacement is one time only. + Rentry = null; + } + + // increment/decrement buttons + AddButton(15, 22 * (i % MaxEntriesPerPage) + 34, 0x15E0, 0x15E4, 6 + i * 2); + AddButton(30, 22 * (i % MaxEntriesPerPage) + 34, 0x15E2, 0x15E6, 7 + i * 2); + + // categorization gump button + AddButton(171 + xoffset - 18, 22 * (i % MaxEntriesPerPage) + 34, 0x15E1, 0x15E5, 5000 + i); + + // goto spawn button + AddButton(171 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 0xFAE, 0xFAF, 1300 + i); + + // text entry gump button + AddButton(200 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 0xFAB, 0xFAD, 800 + i); + + // background for text entry area + AddImageTiled(48, 22 * (i % MaxEntriesPerPage) + 30, 133 + xoffset - 25, 23, 0x52); + AddImageTiled(49, 22 * (i % MaxEntriesPerPage) + 31, 131 + xoffset - 25, 21, background); + + if (i < m_Spawner.SpawnObjects.Length) + { + if (!hasreplacement) + { + str = m_Spawner.SpawnObjects[i].TypeName; + } + + var count = m_Spawner.SpawnObjects[i].SpawnedObjects.Count; + var max = m_Spawner.SpawnObjects[i].ActualMaxCount; + var subgrp = m_Spawner.SpawnObjects[i].SubGroup; + var spawnsper = m_Spawner.SpawnObjects[i].SpawnsPerTick; + + texthue = subgrp * 11; + if (texthue < 0) + { + texthue = 0; + } + + // Add current count + AddImageTiled(231 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 35, 23, 0x52); + AddImageTiled(232 + xoffset, 22 * (i % MaxEntriesPerPage) + 31, 32, 21, 0xBBC); + AddLabel(233 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 68, count.ToString()); + + // Add maximum count + AddImageTiled(267 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 35, 23, 0x52); + AddImageTiled(268 + xoffset, 22 * (i % MaxEntriesPerPage) + 31, 32, 21, 0xBBC); + // AddTextEntry(x,y,w,ht,color,id,str) + AddTextEntry(270 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 30, 30, 33, 500 + i, max.ToString()); + + if (m_ShowGump > 0) + { + // Add subgroup + AddImageTiled(334 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 25, 23, 0x52); + AddImageTiled(335 + xoffset, 22 * (i % MaxEntriesPerPage) + 31, 22, 21, 0xBBC); + AddTextEntry(338 + xoffset, 22 * (i % MaxEntriesPerPage) + 30, 17, 33, texthue, 600 + i, subgrp.ToString()); + } + if (m_ShowGump > 1) + { + // Add subgroup timer fields + string strrst = null; + string strto = null; + string strkill = null; + string strmind = null; + string strmaxd = null; + string strpackrange = null; + var strspawnsper = spawnsper.ToString(); + + if (m_Spawner.SpawnObjects[i].SequentialResetTime > 0 && m_Spawner.SpawnObjects[i].SubGroup > 0) + { + strrst = m_Spawner.SpawnObjects[i].SequentialResetTime.ToString(); + strto = m_Spawner.SpawnObjects[i].SequentialResetTo.ToString(); + } + if (m_Spawner.SpawnObjects[i].KillsNeeded > 0) + { + strkill = m_Spawner.SpawnObjects[i].KillsNeeded.ToString(); + } + + if (m_Spawner.SpawnObjects[i].MinDelay >= 0) + { + strmind = m_Spawner.SpawnObjects[i].MinDelay.ToString(); + } + + if (m_Spawner.SpawnObjects[i].MaxDelay >= 0) + { + strmaxd = m_Spawner.SpawnObjects[i].MaxDelay.ToString(); + } + + if (m_Spawner.SpawnObjects[i].PackRange >= 0) + { + strpackrange = m_Spawner.SpawnObjects[i].PackRange.ToString(); + } + + string strnext; + if (m_Spawner.SpawnObjects[i].NextSpawn > Core.Now) + { + // if the next spawn tick of the spawner will occur after the subgroup is available for spawning + // then report the next spawn tick since that is the earliest that the subgroup can actually be spawned + if (Core.Now + m_Spawner.NextSpawn > m_Spawner.SpawnObjects[i].NextSpawn) + { + strnext = m_Spawner.NextSpawn.ToString(); + } + else + { + // estimate the earliest the next spawn could occur as the first spawn tick after reaching the subgroup nextspawn + strnext = (m_Spawner.SpawnObjects[i].NextSpawn - Core.Now + m_Spawner.NextSpawn).ToString(); + } + } + else + { + strnext = m_Spawner.NextSpawn.ToString(); + } + + var yoff = 22 * (i % MaxEntriesPerPage) + 30; + + // spawns per tick + AddImageTiled(303 + xoffset, yoff, 30, 23, 0x52); + AddImageTiled(304 + xoffset, yoff + 1, 27, 21, 0xBBC); + AddTextEntry(307 + xoffset, yoff, 22, 33, texthue, 1500 + i, strspawnsper); + // reset time + AddImageTiled(329 + xoffset + 30, yoff, 35, 23, 0x52); + AddImageTiled(330 + xoffset + 30, yoff + 1, 32, 21, 0xBBC); + AddTextEntry(333 + xoffset + 30, yoff, 27, 33, texthue, 1000 + i, strrst); + // reset to + AddImageTiled(365 + xoffset + 30, yoff, 26, 23, 0x52); + AddImageTiled(366 + xoffset + 30, yoff + 1, 23, 21, 0xBBC); + AddTextEntry(369 + xoffset + 30, yoff, 18, 33, texthue, 1100 + i, strto); + // kills needed + AddImageTiled(392 + xoffset + 30, yoff, 35, 23, 0x52); + AddImageTiled(393 + xoffset + 30, yoff + 1, 32, 21, 0xBBC); + AddTextEntry(396 + xoffset + 30, yoff, 27, 33, texthue, 1200 + i, strkill); + + // mindelay + AddImageTiled(428 + xoffset + 30, yoff, 41, 23, 0x52); + AddImageTiled(429 + xoffset + 30, yoff + 1, 38, 21, 0xBBC); + AddTextEntry(432 + xoffset + 30, yoff, 33, 33, texthue, 1300 + i, strmind); + + // maxdelay + AddImageTiled(470 + xoffset + 30, yoff, 41, 23, 0x52); + AddImageTiled(471 + xoffset + 30, yoff + 1, 38, 21, 0xBBC); + AddTextEntry(474 + xoffset + 30, yoff, 33, 33, texthue, 1400 + i, strmaxd); + + // packrange + AddImageTiled(512 + xoffset + 30, yoff, 33, 23, 0x52); + AddImageTiled(513 + xoffset + 30, yoff + 1, 30, 21, 0xBBC); + AddTextEntry(516 + xoffset + 30, yoff, 25, 33, texthue, 1600 + i, strpackrange); + + if (m_Spawner.SequentialSpawn >= 0) + { + // restrict kills button + AddButton(545 + xoffset + 30, yoff, m_Spawner.SpawnObjects[i].RestrictKillsToSubgroup ? 0xD3 : 0xD2, + m_Spawner.SpawnObjects[i].RestrictKillsToSubgroup ? 0xD2 : 0xD3, 300 + i); + + //clear on advance button for spawn entries in subgroups that require kills + AddButton(565 + xoffset + 30, yoff, m_Spawner.SpawnObjects[i].ClearOnAdvance ? 0xD3 : 0xD2, + m_Spawner.SpawnObjects[i].ClearOnAdvance ? 0xD2 : 0xD3, 400 + i); + } + + // add the next spawn time + AddLabelCropped(590 + xoffset + 30, yoff, 70, 20, 55, strnext); + } + } + + AddTextEntry(52, 22 * (i % MaxEntriesPerPage) + 31, 119 + xoffset - 25, 21, texthue, i, str); + } + } + + public XmlSpawner.SpawnObject[] CreateArray(RelayInfo info, Mobile from) + { + var SpawnObjects = new ArrayList(); + + for (var i = 0; i < MaxSpawnEntries; i++) + { + var te = info.GetTextEntry(i); + + if (te != null) + { + var str = te.Text; + + if (str.Length > 0) + { + str = str.Trim(); +#if (BOOKTEXTENTRY) + if (i < m_Spawner.SpawnObjects.Length) + { + var currenttext = m_Spawner.SpawnObjects[i].TypeName; + if (currenttext != null && currenttext.Length >= 230) + { + str = currenttext; + } + } +#endif + var typestr = BaseXmlSpawner.ParseObjectType(str); + + var type = AssemblyHandler.FindTypeByName(typestr); + + if (type != null) + { + SpawnObjects.Add(new XmlSpawner.SpawnObject(from, m_Spawner, str, 0)); + } + else + { + // check for special keywords + if (typestr != null && (BaseXmlSpawner.IsTypeOrItemKeyword(typestr) || typestr.IndexOf("{") != -1 || typestr.StartsWith("*") || typestr.StartsWith("#"))) + { + SpawnObjects.Add(new XmlSpawner.SpawnObject(from, m_Spawner, str, 0)); + } + else + { + m_Spawner.status_str = $"{str} is not a valid type name."; + } + //from.SendMessage("{0} is not a valid type name.", str); + } + + } + } + } + + return (XmlSpawner.SpawnObject[])SpawnObjects.ToArray(typeof(XmlSpawner.SpawnObject)); + } + + public void UpdateTypeNames(Mobile from, RelayInfo info) + { + for (var i = 0; i < MaxSpawnEntries; i++) + { + var te = info.GetTextEntry(i); + + if (te != null) + { + var str = te.Text; + + if (str.Length > 0) + { + str = str.Trim(); + if (i < m_Spawner.SpawnObjects.Length) + { + // check to see if the existing typename is longer than the max textentry buffer + // if it is then dont update it since we will assume that the textentry has truncated the actual string + // that could be longer than the buffer if booktextentry is used + +#if (BOOKTEXTENTRY) + var currentstr = m_Spawner.SpawnObjects[i].TypeName; + if (currentstr != null && currentstr.Length < 230) +#endif + { + if (m_Spawner.SpawnObjects[i].TypeName != str) + { + CommandLogging.WriteLine( + from, + $"{from.AccessLevel} {CommandLogging.Format(from)} changed XmlSpawner {m_Spawner.Serial} '{m_Spawner.Name}' [{m_Spawner.GetWorldLocation().X}, {m_Spawner.GetWorldLocation().Y}] ({m_Spawner.Map}) : {m_Spawner.SpawnObjects[i].TypeName} to {str}" + ); + + } + + m_Spawner.SpawnObjects[i].TypeName = str; + } + } + } + } + } + } + + public static void RefreshSpawnerGumps(Mobile from) + { + if (from == null) + { + return; + } + + var ns = from.NetState; + + if (ns?.Gumps != null) + { + var refresh = new ArrayList(); + + foreach (var g in ns.Gumps) + { + // clear the gump status on the spawner associated with the gump + if (g is XmlSpawnerGump xg && xg.m_Spawner != null) + { + refresh.Add(xg); + } + } + + // close all of the currently opened spawner gumps + from.CloseGump(); + + // reopen the closed gumps from the gump collection + foreach (XmlSpawnerGump g in refresh) + { + // reopen a new gump for the spawner + if (g.m_Spawner != null) + { + // flag the current gump on the spawner as closed + g.m_Spawner.GumpReset = true; + + var xg = new XmlSpawnerGump(g.m_Spawner, g.X, g.Y, g.m_ShowGump, g.xoffset, g.page, g.Rentry); + + from.SendGump(xg); + } + } + } + } + + private static bool ValidGotoObject(Mobile from, object o) + { + if (o is Item i) + { + if (!i.Deleted && i.Map != null && i.Map != Map.Internal) + { + return true; + } + + if (from != null && !from.Deleted) + { + from.SendMessage($"{i} is not available"); + } + } + else if (o is Mobile m) + { + if (!m.Deleted && m.Map != null && m.Map != Map.Internal) + { + return true; + } + + if (from != null && !from.Deleted) + { + from.SendMessage($"{m} is not available"); + } + } + + return false; + } + + public override void OnResponse(NetState state, RelayInfo info) + { + if (m_Spawner == null || m_Spawner.Deleted || state == null || info == null) + { + if (m_Spawner != null) + { + m_Spawner.SpawnerGump = null; + } + + return; + } + + // Get the current name + var tr = info.GetTextEntry(999); + if (tr != null) + { + m_Spawner.Name = tr.Text; + } + + // update typenames of the spawn objects based upon the current text entry strings + UpdateTypeNames(state.Mobile, info); + + // Update the creature list + m_Spawner.SpawnObjects = CreateArray(info, state.Mobile); + + if (m_Spawner.SpawnObjects == null) + { + m_Spawner.SpawnerGump = null; + return; + } + + for (var i = 0; i < m_Spawner.SpawnObjects.Length; i++) + { + if (page != i / MaxEntriesPerPage) + { + continue; + } + + // check the max count entry + var temcnt = info.GetTextEntry(500 + i); + if (temcnt != null) + { + var maxval = 0; + try { maxval = Convert.ToInt32(temcnt.Text, 10); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (maxval < 0) + { + maxval = 0; + } + + m_Spawner.SpawnObjects[i].MaxCount = maxval; + } + + if (m_ShowGump > 0) + { + // check the subgroup entry + var tegrp = info.GetTextEntry(600 + i); + if (tegrp != null) + { + var grpval = 0; + try { grpval = Convert.ToInt32(tegrp.Text, 10); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = 0; + } + + m_Spawner.SpawnObjects[i].SubGroup = grpval; + } + } + + if (m_ShowGump > 1) + { + // note, while these values can be entered in any spawn entry, they will only be assigned to the subgroup leader + var subgroupindex = m_Spawner.GetCurrentSequentialSpawnIndex(m_Spawner.SpawnObjects[i].SubGroup); + TextRelay tegrp; + + if (subgroupindex >= 0 && subgroupindex < m_Spawner.SpawnObjects.Length) + { + // check the *reset time* entry + tegrp = info.GetTextEntry(1000 + i); + if (tegrp?.Text != null && tegrp.Text.Length > 0) + { + double grpval = 0; + try { grpval = Convert.ToDouble(tegrp.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = 0; + } + + m_Spawner.SpawnObjects[i].SequentialResetTime = 0; + + m_Spawner.SpawnObjects[subgroupindex].SequentialResetTime = grpval; + } + // check the *reset to* entry + tegrp = info.GetTextEntry(1100 + i); + if (tegrp?.Text != null && tegrp.Text.Length > 0) + { + var grpval = 0; + try { grpval = Convert.ToInt32(tegrp.Text, 10); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = 0; + } + + m_Spawner.SpawnObjects[subgroupindex].SequentialResetTo = grpval; + } + // check the kills entry + tegrp = info.GetTextEntry(1200 + i); + if (tegrp?.Text != null && tegrp.Text.Length > 0) + { + var grpval = 0; + try { grpval = Convert.ToInt32(tegrp.Text, 10); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = 0; + } + + m_Spawner.SpawnObjects[subgroupindex].KillsNeeded = grpval; + } + } + + // check the mindelay + tegrp = info.GetTextEntry(1300 + i); + if (tegrp != null) + { + if (!string.IsNullOrEmpty(tegrp.Text)) + { + double grpval = -1; + try { grpval = Convert.ToDouble(tegrp.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = -1; + } + + // if this value has changed, then update the next spawn time + if (grpval != m_Spawner.SpawnObjects[i].MinDelay) + { + m_Spawner.SpawnObjects[i].MinDelay = grpval; + XmlSpawner.RefreshNextSpawnTime(m_Spawner.SpawnObjects[i]); + } + } + else + { + m_Spawner.SpawnObjects[i].MinDelay = -1; + m_Spawner.SpawnObjects[i].MaxDelay = -1; + XmlSpawner.RefreshNextSpawnTime(m_Spawner.SpawnObjects[i]); + } + } + + // check the maxdelay + tegrp = info.GetTextEntry(1400 + i); + if (tegrp != null) + { + if (!string.IsNullOrEmpty(tegrp.Text)) + { + double grpval = -1; + try { grpval = Convert.ToDouble(tegrp.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = -1; + } + + // if this value has changed, then update the next spawn time + if (grpval != m_Spawner.SpawnObjects[i].MaxDelay) + { + m_Spawner.SpawnObjects[i].MaxDelay = grpval; + XmlSpawner.RefreshNextSpawnTime(m_Spawner.SpawnObjects[i]); + } + } + else + { + m_Spawner.SpawnObjects[i].MinDelay = -1; + m_Spawner.SpawnObjects[i].MaxDelay = -1; + XmlSpawner.RefreshNextSpawnTime(m_Spawner.SpawnObjects[i]); + } + } + + // check the spawns per tick + tegrp = info.GetTextEntry(1500 + i); + if (tegrp != null) + { + if (!string.IsNullOrEmpty(tegrp.Text)) + { + var grpval = 1; + try { grpval = int.Parse(tegrp.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = 1; + } + + // if this value has changed, then update the next spawn time + if (grpval != m_Spawner.SpawnObjects[i].SpawnsPerTick) + { + m_Spawner.SpawnObjects[i].SpawnsPerTick = grpval; + } + } + else + { + m_Spawner.SpawnObjects[i].SpawnsPerTick = 1; + } + } + + // check the packrange + tegrp = info.GetTextEntry(1600 + i); + if (tegrp != null) + { + if (!string.IsNullOrEmpty(tegrp.Text)) + { + var grpval = 1; + try { grpval = int.Parse(tegrp.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (grpval < 0) + { + grpval = 1; + } + + // if this value has changed, then update + if (grpval != m_Spawner.SpawnObjects[i].PackRange) + { + m_Spawner.SpawnObjects[i].PackRange = grpval; + } + } + else + { + m_Spawner.SpawnObjects[i].PackRange = -1; + } + } + } + } + + // Update the maxcount + var temax = info.GetTextEntry(300); + if (temax != null) + { + var maxval = 0; + try { maxval = Convert.ToInt32(temax.Text, 10); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (maxval < 0) + { + maxval = 0; + } + + // if the maxcount of the spawner has been altered external to the interface (e.g. via props, or by the running spawner itself + // then that change will override the text entry + if (m_Spawner.MaxCount == initial_maxcount) + { + m_Spawner.MaxCount = maxval; + } + } + + switch (info.ButtonID) + { + case 0: // Close + { + // clear any text entry books + m_Spawner.DeleteTextEntryBook(); + // and reset the gump status + m_Spawner.GumpReset = true; + + return; + } + case 1: // Help + { + break; + } + case 2: // Bring everything home + { + m_Spawner.BringToHome(); + break; + } + case 3: // Complete respawn + { + m_Spawner.TryRespawn(); + //m_Spawner.AdvanceSequential(); + m_Spawner.KillCount = 0; + break; + } + case 4: // Goto + { + state.Mobile.Location = m_Spawner.Location; + state.Mobile.Map = m_Spawner.Map; + break; + } + case 200: // gump extension + { + state.Mobile.SendGump(m_ShowGump > 1 + ? new XmlSpawnerGump(m_Spawner, X, Y, -1, xoffset, page) + : new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump + 2, xoffset, page)); + return; + } + case 201: // gump text extension + { + state.Mobile.SendGump(xoffset > 0 + ? new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, 0, page) + : new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, 250, page)); + return; + } + case 700: // Start/stop spawner + { + m_Spawner.Running = !m_Spawner.Running; + break; + } + case 701: // Complete reset + { + m_Spawner.Reset(); + break; + } + case 702: // Sort spawns + { + m_Spawner.SortSpawns(); + break; + } + case 900: // empty the status string + { + m_Spawner.status_str = ""; + break; + } + case 9997: + { + m_Spawner.DisableGlobalAutoReset = !m_Spawner.DisableGlobalAutoReset; + break; + } + case 9998: // refresh the gump + { + state.Mobile.SendGump(new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, xoffset, page)); + return; + } + case 9999: + { + // Show the props window for the spawner, as well as a new gump + state.Mobile.SendGump(new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, xoffset, page)); + + state.Mobile.SendGump(new XmlPropertiesGump(state.Mobile, m_Spawner)); + return; + } + default: + { + // check the restrict kills flag + if (info.ButtonID >= 300 && info.ButtonID < 300 + MaxSpawnEntries) + { + var index = info.ButtonID - 300; + if (index < m_Spawner.SpawnObjects.Length) + { + m_Spawner.SpawnObjects[index].RestrictKillsToSubgroup = !m_Spawner.SpawnObjects[index].RestrictKillsToSubgroup; + } + } + else if (info.ButtonID >= 400 && info.ButtonID < 400 + MaxSpawnEntries) + { + var index = info.ButtonID - 400; + if (index < m_Spawner.SpawnObjects.Length) + { + m_Spawner.SpawnObjects[index].ClearOnAdvance = !m_Spawner.SpawnObjects[index].ClearOnAdvance; + } + } + else if (info.ButtonID >= 800 && info.ButtonID < 800 + MaxSpawnEntries) + { + // open the text entry gump + var index = info.ButtonID - 800; + // open a text entry gump +#if (BOOKTEXTENTRY) + // display a new gump + var newgump = new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, xoffset, page); + state.Mobile.SendGump(newgump); + + // is there an existing book associated with the gump? + if (m_Spawner.m_TextEntryBook == null) + { + m_Spawner.m_TextEntryBook = new List(); + } + + var args = new object[6]; + + args[0] = m_Spawner; + args[1] = index; + args[2] = X; + args[3] = Y; + args[4] = m_ShowGump; + args[5] = page; + + var book = new XmlTextEntryBook(0, string.Empty, m_Spawner.Name, 20, true); + + m_Spawner.m_TextEntryBook.Add(book); + + book.Title = $"Entry {index}"; + book.Author = m_Spawner.Name; + + // fill the contents of the book with the current text entry data + var text = string.Empty; + if (m_Spawner.SpawnObjects != null && index < m_Spawner.SpawnObjects.Length) + { + text = m_Spawner.SpawnObjects[index].TypeName; + } + book.FillTextEntryBook(text); + + // put the book at the location of the player so that it can be opened, but drop it below visible range + book.Visible = false; + book.Movable = false; + book.MoveToWorld(new Point3D(state.Mobile.Location.X, state.Mobile.Location.Y, state.Mobile.Location.Z - 100), state.Mobile.Map); + + // and open it + book.OnDoubleClick(state.Mobile); + +#else + state.Mobile.SendGump(new TextEntryGump(m_Spawner,this, index, this.X, this.Y)); +#endif + return; + } + else if (info.ButtonID >= 1300 && info.ButtonID < 1300 + MaxSpawnEntries) + { + nclicks++; + // find the location of the spawn at the specified index + var index = info.ButtonID - 1300; + if (index < m_Spawner.SpawnObjects.Length) + { + var scount = m_Spawner.SpawnObjects[index].SpawnedObjects.Count; + if (scount > 0) + { + var so = m_Spawner.SpawnObjects[index].SpawnedObjects[nclicks % scount]; + + if (ValidGotoObject(state.Mobile, so)) + { + var o = so as IPoint3D; + + if (o != null) + { + var m = m_Spawner.Map; + + if (o is Item item) + { + m = item.Map; + } + + if (o is Mobile mobile) + { + m = mobile.Map; + } + + state.Mobile.Location = new Point3D(o); + state.Mobile.Map = m; + } + } + } + } + } + else if (info.ButtonID >= 4000 && info.ButtonID < 4001 + MaxSpawnEntries / MaxEntriesPerPage) + { + // which page + page = info.ButtonID - 4000; + + } + else if (info.ButtonID >= 6000 && info.ButtonID < 6000 + MaxSpawnEntries) + { + var index = info.ButtonID - 6000; + + if (index < m_Spawner.SpawnObjects.Length) + { + m_Spawner.SpawnObjects[index].Disabled = !m_Spawner.SpawnObjects[index].Disabled; + + // clear any current spawns on the disabled entry + if (m_Spawner.SpawnObjects[index].Disabled) + { + m_Spawner.RemoveSpawnObjects(m_Spawner.SpawnObjects[index]); + } + } + } + else if (info.ButtonID >= 5000 && info.ButtonID < 5000 + MaxSpawnEntries) + { + var i = info.ButtonID - 5000; + + string categorystring = null; + string entrystring = null; + + var te = info.GetTextEntry(i); + + if (te?.Text != null) + { + // get the string + + var cargs = te.Text.Split(','); + + // parse out any comma separated args + categorystring = cargs[0]; + + entrystring = te.Text; + } + + if (string.IsNullOrEmpty(categorystring)) + { + + var newg = new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, xoffset, page); + state.Mobile.SendGump(newg); + + // if no string has been entered then just use the full categorized add gump + state.Mobile.CloseGump(); + state.Mobile.SendGump(new XmlCategorizedAddGump(state.Mobile, i, newg)); + } + else + { + // use the XmlPartialCategorizedAddGump + state.Mobile.CloseGump(); + + //Type [] types = (Type[])XmlPartialCategorizedAddGump.Match(categorystring).ToArray(typeof(Type)); + var types = XmlPartialCategorizedAddGump.Match(categorystring); + + + var re = new ReplacementEntry + { + Typename = entrystring, + Index = i, + Color = 0x1436 + }; + + var newg = new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, xoffset, page, re); + + state.Mobile.SendGump(new XmlPartialCategorizedAddGump(state.Mobile, categorystring, 0, types, true, i, newg)); + + state.Mobile.SendGump(newg); + } + + return; + } + else + { + // up and down arrows + var buttonID = info.ButtonID - 6; + var index = buttonID / 2; + var type = buttonID % 2; + + var entry = info.GetTextEntry(index); + + if (entry != null && entry.Text.Length > 0) + { + var entrystr = entry.Text; + +#if (BOOKTEXTENTRY) + if (index < m_Spawner.SpawnObjects.Length) + { + var str = m_Spawner.SpawnObjects[index].TypeName; + + if (str != null && str.Length >= 230) + { + entrystr = str; + } + } +#endif + if (type == 0) // Add creature + { + m_Spawner.AddSpawnObject(entrystr); + } + else // Remove creatures + { + m_Spawner.DeleteSpawnObject(state.Mobile, entrystr); + + } + } + } + break; + } + } + + state.Mobile.SendGump(new XmlSpawnerGump(m_Spawner, X, Y, m_ShowGump, xoffset, page, Rentry)); + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlSpawnerSkillCheck.cs b/Projects/UOContent/Engines/XMLSpawner/XmlSpawnerSkillCheck.cs new file mode 100644 index 000000000..0cf7bf50b --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlSpawnerSkillCheck.cs @@ -0,0 +1,282 @@ +using System.Collections; +using Server.Items; +using CPA = Server.CommandPropertyAttribute; +using Server.Misc; + +namespace Server.Mobiles; + +public class XmlSpawnerSkillCheck +{ + // alternate skillcheck hooks to replace those in SkillCheck.cs + public static bool Mobile_SkillCheckLocation(Mobile from, SkillName skillName, double minSkill, double maxSkill) + { + var skill = from.Skills[skillName]; + + if (skill == null) + { + return false; + } + + // call the default skillcheck handler + var success = SkillCheck.Mobile_SkillCheckLocation( from, skillName, minSkill, maxSkill); + + // call the xmlspawner skillcheck handler + CheckSkillUse(from, skill, success); + + return success; + } + + public static bool Mobile_SkillCheckDirectLocation(Mobile from, SkillName skillName, double chance) + { + var skill = from.Skills[skillName]; + + if (skill == null) + { + return false; + } + + // call the default skillcheck handler + var success = SkillCheck.Mobile_SkillCheckDirectLocation( from, skillName, chance); + + // call the xmlspawner skillcheck handler + CheckSkillUse(from, skill, success); + + return success; + } + + public static bool Mobile_SkillCheckTarget(Mobile from, SkillName skillName, object target, double minSkill, double maxSkill) + { + var skill = from.Skills[skillName]; + + if (skill == null) + { + return false; + } + + // call the default skillcheck handler + var success = SkillCheck.Mobile_SkillCheckTarget( from, skillName, target, minSkill, maxSkill); + + // call the xmlspawner skillcheck handler + CheckSkillUse(from, skill, success); + + return success; + } + + public static bool Mobile_SkillCheckDirectTarget(Mobile from, SkillName skillName, object target, double chance) + { + var skill = from.Skills[skillName]; + + if (skill == null) + { + return false; + } + + // call the default skillcheck handler + var success = SkillCheck.Mobile_SkillCheckDirectTarget( from, skillName, target, chance); + + // call the xmlspawner skillcheck handler + CheckSkillUse(from, skill, success); + + return success; + } + + + public class RegisteredSkill + { + public const int MaxSkills = 52; + public const SkillName Invalid = (SkillName)(-1); + + public object target; + public SkillName sid; + + // note the extra skill MaxSkills +1 is used for any unknown skill that falls outside of the known 52 + private static ArrayList[] m_FeluccaSkillList = new ArrayList[MaxSkills+1]; + private static ArrayList[] m_TrammelSkillList = new ArrayList[MaxSkills+1]; + private static ArrayList[] m_MalasSkillList = new ArrayList[MaxSkills+1]; + private static ArrayList[] m_IlshenarSkillList = new ArrayList[MaxSkills+1]; + private static ArrayList[] m_TokunoSkillList = new ArrayList[MaxSkills+1]; + + // primary function that returns the list of objects (spawners) that are associated with a given skillname by map + public static ArrayList TriggerList(SkillName index, Map map) + { + if (map == null || map == Map.Internal) + { + return null; + } + + ArrayList[] maplist; + + // get the list for the specified map + + if (map == Map.Felucca) + { + maplist = m_FeluccaSkillList; + } + else if (map == Map.Ilshenar) + { + maplist = m_IlshenarSkillList; + } + else if (map == Map.Malas) + { + maplist = m_MalasSkillList; + } + else if (map == Map.Trammel) + { + maplist = m_TrammelSkillList; + } + else if (map == Map.Tokuno) + { + maplist = m_TokunoSkillList; + } + else + { + return null; + } + + // is it one of the standard 52 skills + if ((int)index >= 0 && (int)index < MaxSkills) + { + return maplist[(int)index] ??= new ArrayList(); + } + + // otherwise pull it out of the final slot for unknown skills. I dont know of a condition that would lead to + // additional skills being registered but it will support them if they are + return maplist[MaxSkills] ??= new ArrayList(); + } + } + + public static void RegisterSkillTrigger(object o, SkillName s, Map map) + { + if (o == null || s == RegisteredSkill.Invalid) + { + return; + } + + // go through the list and if the spawner is not on it yet, then add it + var found = false; + + var skilllist = RegisteredSkill.TriggerList(s, map); + + if (skilllist == null) + { + return; + } + + foreach(RegisteredSkill rs in skilllist) + { + if (rs.target == o && rs.sid == s) + { + found = true; + // dont register a skill if it is already on the list for this spawner + break; + } + } + + // if it hasnt already been added to the list, then add it + if (!found) + { + var newrs = new RegisteredSkill(); + newrs.target = o; + newrs.sid = s; + + skilllist.Add(newrs); + + } + } + + public static void UnRegisterSkillTrigger(object o, SkillName s, Map map, bool all) + { + if (o == null || s == RegisteredSkill.Invalid) + { + return; + } + + // go through the list and if the spawner is on it regardless of the skill registered, then remove it + if (all) + { + for(var i = 0;i 0) + { + foreach(XmlAttachment a in list) + { + if (a != null && !a.Deleted && a.HandlesOnSkillUse) + { + a.OnSkillUse(m, skill, success); + } + } + } + */ + + // then check for registered skills + var skilllist = RegisteredSkill.TriggerList(skill.SkillName, m.Map); + + if (skilllist == null) + { + return; + } + + // determine whether there are any registered objects for this skill + foreach(RegisteredSkill rs in skilllist) + { + // if so then invoke their skill handlers + // call the spawner handler + if (rs.sid == skill.SkillName && rs.target is XmlSpawner spawner && spawner.HandlesOnSkillUse) + { + spawner.OnSkillUse(m, skill, success); + } + } + } + +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlTextEntryBook.cs b/Projects/UOContent/Engines/XMLSpawner/XmlTextEntryBook.cs new file mode 100644 index 000000000..5ed210020 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlTextEntryBook.cs @@ -0,0 +1,80 @@ +namespace Server.Items; + +public class XmlTextEntryBook : BaseBook +{ + public XmlTextEntryBook(int itemID, string title, string author, int pageCount, bool writable) : base(itemID, title, author, pageCount, writable) + { + } + + public XmlTextEntryBook(Serial serial) : base(serial) + { + } + + public void FillTextEntryBook(string text) + { + var pagenum = 0; + var current = 0; + + // break up the text into single line length pieces + while (text != null && current < text.Length) + { + var lineCount = 10; + var lines = new string[lineCount]; + + // place the line on the page + for (var i = 0; i < lineCount; i++) + { + if (current < text.Length) + { + // make each line 25 chars long + var length = text.Length - current; + if (length > 20) + { + length = 20; + } + + lines[i] = text.Substring(current, length); + current += length; + } + else + { + // fill up the remaining lines + lines[i] = string.Empty; + } + } + + if (pagenum >= PagesCount) + { + return; + } + + Pages[pagenum].Lines = lines; + pagenum++; + } + // empty the remaining contents + for (var j = pagenum; j < PagesCount; j++) + { + if (Pages[j].Lines.Length > 0) + { + for (var i = 0; i < Pages[j].Lines.Length; i++) + { + Pages[j].Lines[i] = string.Empty; + } + } + } + } + + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + writer.Write(0); // version + } + + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + reader.ReadInt(); + + Delete(); + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlAdd.cs b/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlAdd.cs new file mode 100644 index 000000000..145895cb8 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlAdd.cs @@ -0,0 +1,1774 @@ +using Server.Accounting; +using Server.Gumps; +using Server.Items; +using Server.Network; +using Server.Targeting; +using System; +using System.Collections; +using System.Data; +using System.IO; + +namespace Server.Mobiles; + +public class XmlSpawnerDefaults +{ + public class DefaultEntry + { + public string AccountName; + public string PlayerName; + public TimeSpan MinDelay = TimeSpan.FromMinutes(5); + public TimeSpan MaxDelay = TimeSpan.FromMinutes(10); + public TimeSpan RefractMin = TimeSpan.FromMinutes(0); + public TimeSpan RefractMax = TimeSpan.FromMinutes(0); + public TimeSpan TODStart = TimeSpan.FromMinutes(0); + public TimeSpan TODEnd = TimeSpan.FromMinutes(0); + public TimeSpan Duration = TimeSpan.FromMinutes(0); + public TimeSpan DespawnTime = TimeSpan.FromHours(0); + public bool Group; + public int Team; + public int ProximitySound = 0x1F4; + public string SpeechTrigger; + public string SkillTrigger; + public int SequentialSpawn = -1; + public bool HomeRangeIsRelative = true; + public int SpawnRange = 5; + public int HomeRange = 5; + public int ProximityRange = -1; + public XmlSpawner.TODModeType TODMode = XmlSpawner.TODModeType.Realtime; + public int KillReset = 1; + public string SpawnerName = "Spawner"; + public bool AllowGhostTrig; + public bool AllowNPCTrig; + public bool SpawnOnTrigger; + public bool SmartSpawning; + public bool ExternalTriggering; + public string TriggerOnCarried; + public string NoTriggerOnCarried; + public string ProximityMsg; + public double TriggerProbability = 1; + public string PlayerTriggerProp; + public string TriggerObjectProp; + public string DefsExt; + public string[] NameList; + public bool[] SelectionList; + public int AddGumpX = 440; + public int AddGumpY; + public int SpawnerGumpX; + public int SpawnerGumpY; + public int FindGumpX; + public int FindGumpY; + + // these are additional defaults that are not set by XmlAdd but can be used by other routines such as the custom properties gump to determine + // whether properties have been changed from spawner default values + public bool Running = true; + + public bool AutoNumber; + public int AutoNumberValue; + + public XmlAddCAGCategory CurrentCategory; + public int CurrentCategoryPage; + public int CategorySelectionIndex = -1; + + public XmlSpawner LastSpawner; + public Map StartingMap; + public Point3D StartingLoc; + public bool ShowExtension; + + public bool IgnoreUpdate; + } + public static ArrayList DefaultEntryList; + + public static DefaultEntry GetDefaults(string account, string name) + { + // find the default entry corresponding to the account and username + if (DefaultEntryList != null) + { + for (var i = 0; i < DefaultEntryList.Count; i++) + { + var entry = (DefaultEntry)DefaultEntryList[i]; + if (entry != null && string.Compare(entry.PlayerName, name, true) == 0 && string.Compare(entry.AccountName, account, true) == 0) + { + return entry; + } + } + } + // if not found then add one + var newentry = new DefaultEntry + { + PlayerName = name, + AccountName = account + }; + if (DefaultEntryList == null) + { + DefaultEntryList = new ArrayList(); + } + + DefaultEntryList.Add(newentry); + return newentry; + } + + public static void RestoreDefs(DefaultEntry defs) + { + if (defs == null) + { + return; + } + + defs.MinDelay = TimeSpan.FromMinutes(5); + defs.MaxDelay = TimeSpan.FromMinutes(10); + defs.RefractMin = TimeSpan.FromMinutes(0); + defs.RefractMax = TimeSpan.FromMinutes(0); + defs.TODStart = TimeSpan.FromMinutes(0); + defs.TODEnd = TimeSpan.FromMinutes(0); + defs.Duration = TimeSpan.FromMinutes(0); + defs.DespawnTime = TimeSpan.FromHours(0); + defs.Group = false; + defs.Team = 0; + defs.ProximitySound = 0x1F4; + defs.SpeechTrigger = null; + defs.SkillTrigger = null; + defs.SequentialSpawn = -1; + defs.HomeRangeIsRelative = true; + defs.SpawnRange = 5; + defs.HomeRange = 5; + defs.ProximityRange = -1; + defs.TODMode = XmlSpawner.TODModeType.Realtime; + defs.KillReset = 1; + defs.SpawnerName = "Spawner"; + defs.AllowGhostTrig = false; + defs.AllowNPCTrig = false; + defs.SpawnOnTrigger = false; + defs.SmartSpawning = false; + defs.ExternalTriggering = false; + defs.TriggerOnCarried = null; + defs.NoTriggerOnCarried = null; + defs.ProximityMsg = null; + defs.TriggerProbability = 1; + defs.PlayerTriggerProp = null; + defs.TriggerObjectProp = null; + defs.DefsExt = null; + defs.AddGumpX = 440; + defs.AddGumpY = 0; + defs.SpawnerGumpX = 0; + defs.SpawnerGumpY = 0; + defs.FindGumpX = 0; + defs.FindGumpY = 0; + defs.AutoNumber = false; + defs.AutoNumberValue = 0; + + if (defs.SelectionList != null) + { + Array.Clear(defs.SelectionList, 0, defs.SelectionList.Length); + } + + if (defs.NameList != null) + { + Array.Clear(defs.NameList, 0, defs.NameList.Length); + } + } +} + +public class XmlAddGump : Gump +{ + private const int MaxEntries = 40; + private const int MaxEntriesPerColumn = 20; + private const string DefsDataSetName = "Defs"; + private const string DefsTablePointName = "Values"; + private const string DefsDir = "SpawnerDefs"; + + private readonly Mobile m_From; + + public XmlSpawnerDefaults.DefaultEntry defs; + + private string NameListToString() + { + if (defs.NameList == null || defs.NameList.Length == 0) + { + return "0"; + } + + var sb = new System.Text.StringBuilder(); + sb.AppendFormat("{0}", defs.NameList.Length); + for (var i = 0; i < defs.NameList.Length; i++) + { + sb.AppendFormat(":{0}", defs.NameList[i]); + } + return sb.ToString(); + } + + private string SelectionListToString() + { + if (defs.SelectionList == null || defs.SelectionList.Length == 0) + { + return "0"; + } + + var sb = new System.Text.StringBuilder(); + sb.AppendFormat("{0}", defs.SelectionList.Length); + for (var i = 0; i < defs.SelectionList.Length; i++) + { + sb.AppendFormat(":{0}", defs.SelectionList[i] ? 1 : 0); + } + return sb.ToString(); + } + + private static string[] StringToNameList(string namelist) + { + var newlist = new string[MaxEntries]; + var tmplist = namelist.Split(':'); + for (var i = 1; i < tmplist.Length; i++) + { + if (i - 1 >= newlist.Length) + { + break; + } + + newlist[i - 1] = tmplist[i]; + } + return newlist; + } + + private static bool[] StringToSelectionList(string selectionlist) + { + var newlist = new bool[MaxEntries]; + var tmplist = selectionlist.Split(':'); + for (var i = 1; i < tmplist.Length; i++) + { + if (i - 1 >= newlist.Length) + { + break; + } + + if (tmplist[i] == "1") + { + newlist[i - 1] = true; + } + else + { + newlist[i - 1] = false; + } + } + return newlist; + } + + private void DoSaveDefs(Mobile from, string filename) + { + if (filename == null || filename.Length <= 0) + { + return; + } + + // Create the data set + var ds = new DataSet(DefsDataSetName); + + // Load the data set up + ds.Tables.Add(DefsTablePointName); + + // Create spawn point schema + //ds.Tables[DefsTablePointName].Columns.Add("AccountName"); + //ds.Tables[DefsTablePointName].Columns.Add("PlayerName"); + ds.Tables[DefsTablePointName].Columns.Add("MinDelay"); + ds.Tables[DefsTablePointName].Columns.Add("MaxDelay"); + ds.Tables[DefsTablePointName].Columns.Add("SpawnRange"); + ds.Tables[DefsTablePointName].Columns.Add("HomeRange"); + ds.Tables[DefsTablePointName].Columns.Add("MinRefractory"); + ds.Tables[DefsTablePointName].Columns.Add("MaxRefractory"); + ds.Tables[DefsTablePointName].Columns.Add("TODStart"); + ds.Tables[DefsTablePointName].Columns.Add("TODEnd"); + ds.Tables[DefsTablePointName].Columns.Add("Duration"); + ds.Tables[DefsTablePointName].Columns.Add("DespawnTime"); + ds.Tables[DefsTablePointName].Columns.Add("RelativeHome"); + ds.Tables[DefsTablePointName].Columns.Add("IsGroup"); + ds.Tables[DefsTablePointName].Columns.Add("Team"); + ds.Tables[DefsTablePointName].Columns.Add("ProximityTriggerSound"); + ds.Tables[DefsTablePointName].Columns.Add("SpeechTrigger"); + ds.Tables[DefsTablePointName].Columns.Add("SkillTrigger"); + ds.Tables[DefsTablePointName].Columns.Add("SequentialSpawn"); + ds.Tables[DefsTablePointName].Columns.Add("ProximityRange"); + ds.Tables[DefsTablePointName].Columns.Add("TODMode"); + ds.Tables[DefsTablePointName].Columns.Add("KillReset"); + ds.Tables[DefsTablePointName].Columns.Add("SpawnerName"); + ds.Tables[DefsTablePointName].Columns.Add("AllowGhost"); + ds.Tables[DefsTablePointName].Columns.Add("AllowNPC"); + ds.Tables[DefsTablePointName].Columns.Add("SpawnOnTrigger"); + ds.Tables[DefsTablePointName].Columns.Add("SmartSpawn"); + ds.Tables[DefsTablePointName].Columns.Add("ExtTrig"); + ds.Tables[DefsTablePointName].Columns.Add("TrigOnCarried"); + ds.Tables[DefsTablePointName].Columns.Add("NoTrigOnCarried"); + ds.Tables[DefsTablePointName].Columns.Add("ProximityMessage"); + ds.Tables[DefsTablePointName].Columns.Add("TrigProb"); + ds.Tables[DefsTablePointName].Columns.Add("PlayerTrigProp"); + ds.Tables[DefsTablePointName].Columns.Add("TrigObjectProp"); + //ds.Tables[DefsTablePointName].Columns.Add("DefsExt"); + ds.Tables[DefsTablePointName].Columns.Add("NameList"); + ds.Tables[DefsTablePointName].Columns.Add("SelectionList"); + ds.Tables[DefsTablePointName].Columns.Add("AddGumpX"); + ds.Tables[DefsTablePointName].Columns.Add("AddGumpY"); + ds.Tables[DefsTablePointName].Columns.Add("SpawnerGumpX"); + ds.Tables[DefsTablePointName].Columns.Add("SpawnerGumpY"); + ds.Tables[DefsTablePointName].Columns.Add("FindGumpX"); + ds.Tables[DefsTablePointName].Columns.Add("FindGumpY"); + ds.Tables[DefsTablePointName].Columns.Add("AutoNumber"); + ds.Tables[DefsTablePointName].Columns.Add("AutoNumberValue"); + + // Create a new data row + var dr = ds.Tables[DefsTablePointName].NewRow(); + + // Populate the data + //dr["AccountName"] = (string)defs.AccountName; + //dr["PlayerName"] = (string)defs.PlayerName; + dr["SpawnerName"] = defs.SpawnerName; + dr["MinDelay"] = defs.MinDelay.TotalMinutes; + dr["MaxDelay"] = defs.MaxDelay.TotalMinutes; + dr["SpawnRange"] = defs.SpawnRange; + dr["HomeRange"] = defs.HomeRange; + dr["RelativeHome"] = defs.HomeRangeIsRelative; + dr["IsGroup"] = defs.Group; + dr["Team"] = defs.Team; + dr["MinRefractory"] = defs.RefractMin.TotalMinutes; + dr["MaxRefractory"] = defs.RefractMax.TotalMinutes; + dr["TODStart"] = defs.TODStart.TotalMinutes; + dr["TODEnd"] = defs.TODEnd.TotalMinutes; + dr["TODMode"] = defs.TODMode; + dr["Duration"] = defs.Duration.TotalMinutes; + dr["DespawnTime"] = defs.Duration.TotalHours; + dr["ProximityRange"] = defs.ProximityRange; + dr["ProximityTriggerSound"] = defs.ProximitySound; + dr["ProximityMessage"] = defs.ProximityMsg; + dr["SpeechTrigger"] = defs.SpeechTrigger; + dr["SkillTrigger"] = defs.SkillTrigger; + dr["SequentialSpawn"] = defs.SequentialSpawn; + dr["KillReset"] = defs.KillReset; + dr["TrigProb"] = defs.TriggerProbability; + dr["AllowGhost"] = defs.AllowGhostTrig; + dr["AllowNPC"] = defs.AllowNPCTrig; + dr["SpawnOnTrigger"] = defs.SpawnOnTrigger; + dr["SmartSpawn"] = defs.SmartSpawning; + dr["ExtTrig"] = defs.ExternalTriggering; + dr["TrigOnCarried"] = defs.TriggerOnCarried; + dr["NoTrigOnCarried"] = defs.NoTriggerOnCarried; + dr["PlayerTrigProp"] = defs.PlayerTriggerProp; + dr["TrigObjectProp"] = defs.TriggerObjectProp; + dr["NameList"] = NameListToString(); + dr["SelectionList"] = SelectionListToString(); + dr["AddGumpX"] = defs.AddGumpX; + dr["AddGumpY"] = defs.AddGumpY; + dr["SpawnerGumpX"] = defs.SpawnerGumpX; + dr["SpawnerGumpY"] = defs.SpawnerGumpY; + dr["FindGumpX"] = defs.FindGumpX; + dr["FindGumpY"] = defs.FindGumpY; + dr["AutoNumber"] = defs.AutoNumber; + dr["AutoNumberValue"] = defs.AutoNumberValue; + + // Add the row the the table + ds.Tables[DefsTablePointName].Rows.Add(dr); + + // Write out the file + var file_error = false; + + var dirname = Directory.Exists(DefsDir) ? $"{DefsDir}/{filename}.defs" : $"{filename}.defs"; + + try + { + ds.WriteXml(dirname); + } + catch { file_error = true; } + + if (file_error) + { + if (from != null && !from.Deleted) + { + from.SendMessage($"Error trying to save to file {dirname}"); + } + + return; + } + + if (from != null && !from.Deleted) + { + from.SendMessage($"Saved defs to file {dirname}"); + } + } + + private void DoLoadDefs(Mobile from, string filename) + { + if (filename == null || filename.Length <= 0) + { + return; + } + + string dirname; + if (Directory.Exists(DefsDir)) + { + // look for it in the defaults directory + dirname = $"{DefsDir}/{filename}.defs"; + // Check if the file exists + if (File.Exists(dirname) == false) + { + // didnt find it so just look in the main install dir + dirname = $"{filename}.defs"; + } + } + else + { + // look in the main installation dir + dirname = $"{filename}.defs"; + } + // Check if the file exists + if (File.Exists(dirname)) + { + FileStream fs = null; + try + { + fs = File.Open(dirname, FileMode.Open, FileAccess.Read); + } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + + if (fs == null) + { + from.SendMessage($"Unable to open {dirname} for loading"); + return; + } + + // Create the data set + var ds = new DataSet(DefsDataSetName); + + // Read in the file + //ds.ReadXml(e.Arguments[0].ToString()); + var fileerror = false; + try + { + ds.ReadXml(fs); + } + catch { fileerror = true; } + // close the file + fs.Close(); + if (fileerror) + { + if (from != null && !from.Deleted) + { + from.SendMessage(33, $"Error reading defs file {dirname}"); + } + + return; + } + + // Check that at least a single table was loaded + if (ds.Tables.Count > 0) + { + // Add each spawn point to the current map + if (ds.Tables[DefsTablePointName] != null && ds.Tables[DefsTablePointName].Rows.Count > 0) + { + //foreach(DataRow dr in ds.Tables[DefsTablePointName].Rows){ + var dr = ds.Tables[DefsTablePointName].Rows[0]; + + try { defs.SpawnerName = (string)dr["SpawnerName"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + + var mindelay = defs.MinDelay.TotalMinutes; + try { mindelay = double.Parse((string)dr["MinDelay"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.MinDelay = TimeSpan.FromMinutes(mindelay); + + var maxdelay = defs.MaxDelay.TotalMinutes; + try { maxdelay = double.Parse((string)dr["MaxDelay"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.MaxDelay = TimeSpan.FromMinutes(maxdelay); + + try { defs.SpawnRange = int.Parse((string)dr["SpawnRange"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.HomeRange = int.Parse((string)dr["HomeRange"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.HomeRangeIsRelative = bool.Parse((string)dr["RelativeHome"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.Group = bool.Parse((string)dr["IsGroup"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.Team = int.Parse((string)dr["Team"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + + var minrefract = defs.RefractMin.TotalMinutes; + try { minrefract = double.Parse((string)dr["MinRefractory"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.RefractMin = TimeSpan.FromMinutes(minrefract); + + var maxrefract = defs.RefractMax.TotalMinutes; + try { maxrefract = double.Parse((string)dr["MaxRefractory"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.RefractMax = TimeSpan.FromMinutes(maxrefract); + + var todstart = defs.TODStart.TotalMinutes; + try { todstart = double.Parse((string)dr["TODStart"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.TODStart = TimeSpan.FromMinutes(todstart); + + var todend = defs.TODEnd.TotalMinutes; + try { todend = double.Parse((string)dr["TODEnd"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.TODEnd = TimeSpan.FromMinutes(todend); + + string todmode = null; + try { todmode = (string)dr["TODMode"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + if (todmode != null) + { + if (todmode == "Realtime") + { + defs.TODMode = XmlSpawner.TODModeType.Realtime; + } + else + if (todmode == "Gametime") + { + defs.TODMode = XmlSpawner.TODModeType.Gametime; + } + } + + var duration = defs.Duration.TotalMinutes; + try { duration = double.Parse((string)dr["Duration"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.Duration = TimeSpan.FromMinutes(duration); + + var despawnTime = defs.DespawnTime.TotalHours; + try { despawnTime = double.Parse((string)dr["DespawnTime"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + defs.DespawnTime = TimeSpan.FromHours(despawnTime); + + try { defs.ProximityRange = int.Parse((string)dr["ProximityRange"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.ProximitySound = int.Parse((string)dr["ProximityTriggerSound"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.ProximityMsg = (string)dr["ProximityMessage"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SpeechTrigger = (string)dr["SpeechTrigger"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SkillTrigger = (string)dr["SkillTrigger"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SequentialSpawn = int.Parse((string)dr["SequentialSpawn"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.KillReset = int.Parse((string)dr["KillReset"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.TriggerProbability = double.Parse((string)dr["TrigProb"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.AllowGhostTrig = bool.Parse((string)dr["AllowGhost"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.AllowNPCTrig = bool.Parse((string)dr["AllowNPC"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SpawnOnTrigger = bool.Parse((string)dr["SpawnOnTrigger"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SmartSpawning = bool.Parse((string)dr["SmartSpawn"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.ExternalTriggering = bool.Parse((string)dr["ExtTrig"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.TriggerOnCarried = (string)dr["TrigOnCarried"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.NoTriggerOnCarried = (string)dr["NoTrigOnCarried"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.PlayerTriggerProp = (string)dr["PlayerTrigProp"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.TriggerObjectProp = (string)dr["TrigObjectProp"]; } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + + try { defs.NameList = StringToNameList((string)dr["NameList"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SelectionList = StringToSelectionList((string)dr["SelectionList"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.AddGumpX = int.Parse((string)dr["AddGumpX"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.AddGumpY = int.Parse((string)dr["AddGumpY"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SpawnerGumpX = int.Parse((string)dr["SpawnerGumpX"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.SpawnerGumpY = int.Parse((string)dr["SpawnerGumpY"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.FindGumpX = int.Parse((string)dr["FindGumpX"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.FindGumpY = int.Parse((string)dr["FindGumpY"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.AutoNumber = bool.Parse((string)dr["AutoNumber"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + try { defs.AutoNumberValue = int.Parse((string)dr["AutoNumberValue"]); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + + if (from != null && !from.Deleted) + { + from.SendMessage($"Loaded defs from file {dirname}"); + } + } + } + } + else + { + if (from != null && !from.Deleted) + { + from.SendMessage(33, $"File not found: {dirname}"); + } + } + } + + public static void Initialize() + { + CommandSystem.Register("XmlAdd", AccessLevel.GameMaster, XmlAdd_OnCommand); + } + + [Usage("XmlAdd [-defaults]")] + [Description("Opens a gump that can add Xmlspawners with specified default settings")] + public static void XmlAdd_OnCommand(CommandEventArgs e) + { + var acct = e.Mobile.Account as Account; + var x = 440; + var y = 0; + XmlSpawnerDefaults.DefaultEntry defs = null; + if (acct != null) + { + defs = XmlSpawnerDefaults.GetDefaults(acct.ToString(), e.Mobile.Name); + } + + if (defs != null) + { + x = defs.AddGumpX; + y = defs.AddGumpY; + } + // Check if there is an argument provided (load criteria) + try + { + // Check if there is an argument provided (load criteria) + for (var nxtarg = 0; nxtarg < e.Arguments.Length; nxtarg++) + { + // is it a defaults option? + if (e.Arguments[nxtarg].ToLower() == "-defaults") + { + XmlSpawnerDefaults.RestoreDefs(defs); + if (defs != null) + { + x = defs.AddGumpX; + y = defs.AddGumpY; + } + } + } + } + catch (Exception ex) { Diagnostics.ExceptionLogging.LogException(ex); } + + e.Mobile.SendGump(new XmlAddGump(e.Mobile, e.Mobile.Location, e.Mobile.Map, true, false, x, y)); + + } + + public XmlAddGump(Mobile from, Point3D startloc, Map startmap, bool firststart, bool extension, int gumpx, int gumpy) : base(gumpx, gumpy) + { + if (from == null || from.Deleted) + { + return; + } + + defs = null; + + m_From = from; + + // read the text entries for default values + var acct = from.Account as Account; + if (acct != null) + { + defs = XmlSpawnerDefaults.GetDefaults(acct.ToString(), from.Name); + } + + if (defs == null) + { + return; + } + + if (firststart) + { + defs.StartingMap = from.Map; + defs.StartingLoc = from.Location; + } + else + { + defs.StartingMap = startmap; + defs.StartingLoc = startloc; + } + + defs.IgnoreUpdate = false; + defs.ShowExtension = extension; + + if (defs.SelectionList == null) + { + defs.SelectionList = new bool[MaxEntries]; + } + if (defs.NameList == null) + { + defs.NameList = new string[MaxEntries]; + } + + + // prepare the page + + AddPage(0); + if (defs.ShowExtension) + { + AddBackground(0, 0, 520, 500, 5054); + AddAlphaRegion(0, 0, 520, 500); + } + else + { + AddBackground(0, 0, 200, 500, 5054); + AddAlphaRegion(0, 0, 200, 500); + } + + var y = 3; + var yinc = 20; + // add the min/maxdelay entries + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 100, defs.MinDelay.TotalMinutes.ToString()); + AddLabel(45, y, 0x384, "MinDelay(m)"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 101, defs.MaxDelay.TotalMinutes.ToString()); + AddLabel(145, y, 0x384, "MaxDelay(m)"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 107, defs.HomeRange.ToString()); + AddLabel(45, y, 0x384, "HomeRng"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 108, defs.SpawnRange.ToString()); + AddLabel(145, y, 0x384, "SpawnRng"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 109, defs.ProximityRange.ToString()); + AddLabel(45, y, 0x384, "ProxRng"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 110, defs.Team.ToString()); + AddLabel(145, y, 0x384, "Team"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 113, defs.KillReset.ToString()); + AddLabel(45, y, 0x384, "KillReset"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 121, defs.TriggerProbability.ToString()); + AddLabel(145, y, 0x384, "TrigProb"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 111, defs.Duration.TotalMinutes.ToString()); + AddLabel(45, y, 0x384, "Duration(m)"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 112, defs.ProximitySound.ToString()); + AddLabel(145, y, 0x384, "ProxSnd"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 102, defs.RefractMin.TotalMinutes.ToString()); + AddLabel(45, y, 0x384, "MinRefr(m)"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 103, defs.RefractMax.TotalMinutes.ToString()); + AddLabel(145, y, 0x384, "MaxRefr(m)"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 104, defs.TODStart.TotalHours.ToString()); + AddLabel(45, y, 0x384, "TODStart(h)"); + + AddImageTiled(105, y, 40, 19, 0xBBC); + AddTextEntry(105, y, 40, 19, 0, 105, defs.TODEnd.TotalHours.ToString()); + AddLabel(145, y, 0x384, "TODEnd(h)"); + + y += yinc; + AddImageTiled(5, y, 40, 19, 0xBBC); + AddTextEntry(5, y, 40, 19, 0, 123, defs.DespawnTime.TotalHours.ToString()); + AddLabel(45, y, 0x384, "Despawn(h)"); + + + // AllowNPC + AddLabel(125, y, 0x384, "AllowNPC"); + AddCheck(105, y, 0xD2, 0xD3, defs.AllowNPCTrig, 312); + + //y = 164; + yinc = 21; + y += yinc; + // TOD + if (defs.TODMode == XmlSpawner.TODModeType.Gametime) + { + AddLabel(25, y, 0x384, "GameTOD"); + } + else + if (defs.TODMode == XmlSpawner.TODModeType.Realtime) + { + AddLabel(25, y, 0x384, "RealTOD"); + } + + AddButton(5, y, 0xD3, 0xD3, 306); + + + // Sequentialspawn + AddLabel(125, y, 0x384, "SeqSpawn"); + AddCheck(105, y, 0xD2, 0xD3, defs.SequentialSpawn == 0, 307); + + y += yinc; + // IsGroup + AddLabel(25, y, 0x384, "Group"); + AddCheck(5, y, 0xD2, 0xD3, defs.Group, 304); + + // HomeRangeRelative + AddLabel(125, y, 0x384, "HomeRngRel"); + AddCheck(105, y, 0xD2, 0xD3, defs.HomeRangeIsRelative, 305); + + y += yinc; + // smart spawning + AddLabel(25, y, 0x384, "SmartSpawn"); + AddCheck(5, y, 0xD2, 0xD3, defs.SmartSpawning, 310); + + // AllowGhost + AddLabel(125, y, 0x384, "AllowGhost"); + AddCheck(105, y, 0xD2, 0xD3, defs.AllowGhostTrig, 309); + + y += yinc; + // ExtTrig + AddLabel(25, y, 0x384, "ExtTrig"); + AddCheck(5, y, 0xD2, 0xD3, defs.ExternalTriggering, 308); + + // SpawnOnTrig + AddLabel(125, y, 0x384, "SpawnOnTrig"); + AddCheck(105, y, 0xD2, 0xD3, defs.SpawnOnTrigger, 311); + + y += yinc; + // AutoNumber + AddLabel(25, y, 0x384, "AutoNumber"); + AddCheck(5, y, 0xD2, 0xD3, defs.AutoNumber, 306); + AddImageTiled(105, y, 80, 19, 0xBBC); + AddTextEntry(105, y, 80, 19, 0, 125, defs.AutoNumberValue.ToString()); + + //y = 270; + yinc = 20; + y += yinc; + // Name + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 114, defs.SpawnerName); + AddLabel(105, y, 0x384, "SpawnerName"); + + y += yinc; + // speech trigger + AddLabel(105, y, 0x384, "SpeechTrigger"); + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 106, defs.SpeechTrigger); + + y += yinc; + // skill trigger + AddLabel(105, y, 0x384, "SkillTrigger"); + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 124, defs.SkillTrigger); + + y += yinc; + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 117, defs.TriggerOnCarried); + AddLabel(105, y, 0x384, "TrigOnCarried"); + + y += yinc; + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 118, defs.NoTriggerOnCarried); + AddLabel(105, y, 0x384, "NoTrigOnCarried"); + + y += yinc; + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 119, defs.ProximityMsg); + AddLabel(105, y, 0x384, "ProximityMsg"); + + y += yinc; + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 120, defs.PlayerTriggerProp); + AddLabel(105, y, 0x384, "PlayerTrigProp"); + + y += yinc; + AddImageTiled(5, y, 95, 19, 0xBBC); + AddTextEntry(5, y, 85, 19, 0, 122, defs.TriggerObjectProp); + AddLabel(105, y, 0x384, "TrigObjectProp"); + + //y = 429; + yinc = 23; + y += yinc; + // add the RestoreDefs button + AddButton(5, y, 0xFAE, 0xFAF, 117); + AddLabel(35, y, 0x384, "Restore Defs"); + + // add the RestoreDefs button + AddButton(125, y, 0xFAE, 0xFAF, 180); + AddLabel(155, y, 0x384, "Options"); + + y += yinc; + // add the SaveDefs button + AddButton(5, y, 0xFAE, 0xFAF, 115); + AddLabel(35, y, 0x384, "Save"); + + // add the LoadDefs button + AddButton(65, y, 0xFAE, 0xFAF, 116); + AddLabel(95, y, 0x384, "Load"); + + // add the DefsExt entry + AddImageTiled(127, y, 68, 21, 0xBBC); + AddTextEntry(129, y, 64, 21, 0, 115, defs.DefsExt); + + y += yinc; + // add the Add button + AddButton(5, y, 0xFAE, 0xFAF, 100); + AddLabel(35, y, 0x384, "Add"); + + // add the Goto button + AddButton(64, y, 0xFAE, 0xFAF, 1000); + AddLabel(94, y, 0x384, "Goto"); + + // add the Delete button + AddButton(125, y, 0xFAE, 0xFAF, 156); + AddLabel(155, y, 0x384, "Del"); + + // add the Edit button + // add the Find button + + // add gump extension button + if (defs.ShowExtension) + { + AddButton(480, y + 5, 0x15E3, 0x15E7, 200); + } + else + { + AddButton(180, y + 5, 0x15E1, 0x15E5, 200); + } + + if (defs.ShowExtension) + { + AddLabel(300, 5, 0x384, "Spawn Entries"); + // display the clear all toggle + AddButton(475, 5, 0xD2, 0xD3, 3999); + // display the selection entries + for (var i = 0; i < MaxEntries; i++) + { + var xpos = i / MaxEntriesPerColumn * 155; + var ypos = i % MaxEntriesPerColumn * 22 + 30; + + // background for search results area + AddImageTiled(xpos + 205, ypos, 116, 23, 0x52); + + // has this been selected for category info specification? + AddImageTiled(xpos + 206, ypos + 1, 114, 21, i == defs.CategorySelectionIndex ? 0x1436 : 0xBBC); + + var sel = false; + if (defs.SelectionList != null && i < defs.SelectionList.Length) + { + sel = defs.SelectionList[i]; + } + + var texthue = 0; + if (sel) + { + texthue = 68; + } + + string namestr = null; + if (defs.NameList != null && i < defs.NameList.Length) + { + namestr = defs.NameList[i]; + } + + AddTextEntry(xpos + 208, ypos + 1, 110, 21, texthue, 1000 + i, namestr); + // display the selection button + AddButton(xpos + 320, ypos + 2, sel ? 0xD3 : 0xD2, sel ? 0xD2 : 0xD3, 4000 + i); + // display the info button + AddButton(xpos + 340, ypos + 2, 0x15E1, 0x15E5, 5000 + i); + } + } + } + + private void DoGoTo(Item x) + { + if (m_From == null || m_From.Deleted) + { + return; + } + + if (x == null || x.Deleted || x.Map == null) + { + return; + } + + Point3D itemloc; + + if (x.Parent != null) + { + if (x.RootParent is Container container) + { + itemloc = container.Location; + } + else + { + return; + } + } + else + { + itemloc = x.Location; + } + m_From.Location = itemloc; + m_From.Map = x.Map; + } + + private void DoShowProps(IEntity x) + { + if (m_From == null || m_From.Deleted) + { + return; + } + + if (x == null || x.Deleted || x.Map == null) + { + return; + } + + m_From.SendGump(new PropertiesGump(m_From, x)); + } + + private static void DoShowGump(Mobile from, Item x) + { + if (from == null || from.Deleted) + { + return; + } + + if (x == null || x.Deleted || x.Map == null || x.Map == Map.Internal) + { + return; + } + + x.OnDoubleClick(from); + } + + public static void Refresh(Mobile from) + { + Refresh(from, false); + } + + public static void Refresh(Mobile from, bool ignoreupdate) + { + if (from == null) + { + return; + } + + // read the text entries for default values + XmlSpawnerDefaults.DefaultEntry defs = null; + + var acct = from.Account as Account; + if (acct != null) + { + defs = XmlSpawnerDefaults.GetDefaults(acct.ToString(), from.Name); + } + + if (defs == null) + { + return; + } + + var x = defs.AddGumpX; + var y = defs.AddGumpY; + if (defs.ShowExtension) + { + // shift the starting point + x = defs.AddGumpX - 140; + } + + defs.IgnoreUpdate = ignoreupdate; + + from.CloseGump(); + + defs.IgnoreUpdate = false; + from.SendGump(new XmlAddGump(from, defs.StartingLoc, defs.StartingMap, false, defs.ShowExtension, x, y)); + } + + private class PlaceSpawnerTarget : Target + { + readonly XmlSpawnerDefaults.DefaultEntry defs; + readonly NetState m_state; + + public PlaceSpawnerTarget(NetState state) : base(30, true, TargetFlags.None) + { + if (state?.Mobile == null) + { + return; + } + + // read the text entries for default values + defs = null; + + var acct = state.Mobile.Account as Account; + if (acct != null) + { + defs = XmlSpawnerDefaults.GetDefaults(acct.ToString(), state.Mobile.Name); + } + + if (defs == null) + { + return; + } + + m_state = state; + } + + protected override void OnTarget(Mobile from, object targeted) + { + if (from == null) + { + return; + } + + // assign it a unique id + var SpawnId = Guid.NewGuid(); + // count the number of entries to be added for maxcount + var maxcount = 0; + for (var i = 0; i < MaxEntries; i++) + { + if (defs.SelectionList != null && i < defs.SelectionList.Length && defs.SelectionList[i] && + defs.NameList != null && i < defs.NameList.Length && defs.NameList[i] != null && defs.NameList[i].Length > 0) + { + maxcount++; + } + } + + // if autonumbering is enabled, name the spawner with the name+number + var sname = defs.SpawnerName; + if (defs.AutoNumber) + { + sname = $"{defs.SpawnerName}#{defs.AutoNumberValue}"; + } + + var spawner = new XmlSpawner(SpawnId, from.Location.X, from.Location.Y, 0, 0, sname, maxcount, + defs.MinDelay, defs.MaxDelay, defs.Duration, defs.ProximityRange, defs.ProximitySound, 1, + defs.Team, defs.HomeRange, defs.HomeRangeIsRelative, new XmlSpawner.SpawnObject[0], defs.RefractMin, defs.RefractMax, + defs.TODStart, defs.TODEnd, null, defs.TriggerObjectProp, defs.ProximityMsg, defs.TriggerOnCarried, defs.NoTriggerOnCarried, + defs.SpeechTrigger, null, null, defs.PlayerTriggerProp, defs.TriggerProbability, null, defs.Group, defs.TODMode, defs.KillReset, defs.ExternalTriggering, + defs.SequentialSpawn, null, defs.AllowGhostTrig, defs.AllowNPCTrig, defs.SpawnOnTrigger, null, defs.DespawnTime, defs.SkillTrigger, defs.SmartSpawning, null) + { + PlayerCreated = true + }; + + // if the object is a container, then place it in the container + if (targeted is Container container) + { + container.DropItem(spawner); + } + else + { + // place the spawner at the targeted location + var p = targeted as IPoint3D; + if (p == null) + { + spawner.Delete(); + return; + } + if (p is Item item) + { + p = item.GetWorldTop(); + } + + spawner.MoveToWorld(new Point3D(p), from.Map); + } + + spawner.SpawnRange = defs.SpawnRange; + // add entries from the name list + for (var i = 0; i < MaxEntries; i++) + { + if (defs.SelectionList != null && i < defs.SelectionList.Length && defs.SelectionList[i] && + defs.NameList != null && i < defs.NameList.Length && defs.NameList[i] != null && defs.NameList[i].Length > 0) + { + spawner.AddSpawn = defs.NameList[i]; + } + } + + defs.LastSpawner = spawner; + + if (defs.AutoNumber) + // bump the autonumber + { + defs.AutoNumberValue++; + } + + //from.CloseGump(); + Refresh(m_state.Mobile, true); + + // open the spawner gump + DoShowGump(from, spawner); + } + } + + public override void OnResponse(NetState state, RelayInfo info) + { + if (info == null || state?.Mobile == null) + { + return; + } + + // read the text entries for default values + var defaults = XmlSpawnerDefaults.GetDefaults(state.Account.ToString(), state.Mobile.Name); + if (defaults.IgnoreUpdate) + { + return; + } + + var tr = info.GetTextEntry(100); // mindelay + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.MinDelay = TimeSpan.FromMinutes(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + tr = info.GetTextEntry(101); // maxdelay info + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.MaxDelay = TimeSpan.FromMinutes(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(102); // min refractory + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.RefractMin = TimeSpan.FromMinutes(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(103); // max refractory + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.RefractMax = TimeSpan.FromMinutes(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(104); // TOD start + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.TODStart = TimeSpan.FromHours(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(105); // TOD end + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.TODEnd = TimeSpan.FromHours(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(106); // Speech trigger + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.SpeechTrigger = txt; + } + + tr = info.GetTextEntry(107); // HomeRange + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.HomeRange = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(108); // SpawnRange + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.SpawnRange = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(109); // ProximityRange + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.ProximityRange = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(110); // Team + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.Team = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(111); // Duration + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.Duration = TimeSpan.FromMinutes(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(112); // ProximitySound + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.ProximitySound = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(113); // Kill reset + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.KillReset = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(114); // Spawner name + if (tr != null) + { + defaults.SpawnerName = tr.Text; + } + + // DefsExt entry + tr = info.GetTextEntry(115); // save def str + if (tr != null) + { + defaults.DefsExt = tr.Text; + } + + tr = info.GetTextEntry(117); // trigger on carried + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.TriggerOnCarried = txt; + } + + tr = info.GetTextEntry(118); // no trigger on carried + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.NoTriggerOnCarried = txt; + } + + tr = info.GetTextEntry(119); // proximity message + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.ProximityMsg = txt; + } + + tr = info.GetTextEntry(120); // player trig prop + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.PlayerTriggerProp = txt; + } + + tr = info.GetTextEntry(121); // Trigger probability + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.TriggerProbability = double.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(122); // trig object prop + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.TriggerObjectProp = txt; + } + + tr = info.GetTextEntry(123); // DespawnTime + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.DespawnTime = TimeSpan.FromHours(double.Parse(tr.Text)); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + tr = info.GetTextEntry(124); // Skill trigger + if (tr != null) + { + var txt = tr.Text; + if (txt != null && txt.Length == 0) + { + txt = null; + } + + defaults.SkillTrigger = txt; + } + + tr = info.GetTextEntry(125); // AutoNumberValue + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defaults.AutoNumberValue = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + + // fill the NameList from the text entries + if (defaults.ShowExtension) + { + for (var i = 0; i < MaxEntries; i++) + { + tr = info.GetTextEntry(1000 + i); + if (defaults.NameList != null && i < defaults.NameList.Length && tr != null) + { + defaults.NameList[i] = tr.Text; + } + } + } + + defaults.Group = info.IsSwitched(304); + defaults.HomeRangeIsRelative = info.IsSwitched(305); + defaults.AutoNumber = info.IsSwitched(306); + defaults.SequentialSpawn = info.IsSwitched(307) ? 0 : -1; + defaults.ExternalTriggering = info.IsSwitched(308); + defaults.AllowGhostTrig = info.IsSwitched(309); + defaults.SpawnOnTrigger = info.IsSwitched(311); + defaults.SmartSpawning = info.IsSwitched(310); + defaults.AllowNPCTrig = info.IsSwitched(312); + + switch (info.ButtonID) + { + case 0: // Close + { + return; + } + case 100: // Add spawner + { + state.Mobile.Target = new PlaceSpawnerTarget(state); + + break; + } + case 115: // SaveDefs + { + string filename; + if (!string.IsNullOrEmpty(defaults.DefsExt)) + { + filename = $"{defaults.AccountName}-{defaults.PlayerName}-{defaults.DefsExt}"; + } + else + { + filename = $"{defaults.AccountName}-{defaults.PlayerName}"; + } + DoSaveDefs(state.Mobile, filename); + break; + } + case 116: // LoadDefs + { + string filename; + if (!string.IsNullOrEmpty(defaults.DefsExt)) + { + filename = $"{defaults.AccountName}-{defaults.PlayerName}-{defaults.DefsExt}"; + } + else + { + filename = $"{defaults.AccountName}-{defaults.PlayerName}"; + } + DoLoadDefs(state.Mobile, filename); + break; + } + case 117: // Restore Defaults + { + state.Mobile.SendMessage("Restoring defaults"); + XmlSpawnerDefaults.RestoreDefs(defaults); + break; + } + case 155: // Return the player to the starting loc + { + m_From.Location = defaults.StartingLoc; + m_From.Map = defaults.StartingMap; + break; + } + case 156: // Delete last spawner + { + if (defaults.LastSpawner == null || defaults.LastSpawner.Deleted) + { + break; + } + + Refresh(state.Mobile); + state.Mobile.SendGump(new XmlAddConfirmDeleteGump(defaults.LastSpawner)); + return; + } + case 157: // Reset last spawner + { + if (defaults.LastSpawner != null && !defaults.LastSpawner.Deleted) + { + defaults.LastSpawner.DoReset = true; + } + + break; + } + case 158: // Respawn last spawner + { + if (defaults.LastSpawner != null && !defaults.LastSpawner.Deleted) + { + defaults.LastSpawner.DoRespawn = true; + } + + break; + } + case 180: // Set Options + { + Refresh(state.Mobile); + state.Mobile.SendGump(new XmlAddOptionsGump(state.Mobile)); + return; + } + case 200: // gump extension + { + defaults.ShowExtension = !defaults.ShowExtension; + break; + } + case 306: // TOD mode + { + defaults.TODMode = defaults.TODMode == XmlSpawner.TODModeType.Realtime ? XmlSpawner.TODModeType.Gametime : XmlSpawner.TODModeType.Realtime; + break; + } + case 1000: // GoTo + { + // then go to it + DoGoTo(defaults.LastSpawner); + break; + } + case 1001: // Show Gump + { + Refresh(state.Mobile); + DoShowGump(state.Mobile, defaults.LastSpawner); + break; + } + case 1002: // Show Props + { + Refresh(state.Mobile); + DoShowProps(defaults.LastSpawner); + break; + } + case 3999: // clear selections + { + // clear the selections + if (defaults.SelectionList != null) + { + Array.Clear(defaults.SelectionList, 0, defaults.SelectionList.Length); + } + + break; + } + case 9998: // refresh the gump + { + break; + } + default: + { + if (info.ButtonID >= 4000 && info.ButtonID < 4000 + MaxEntries) + { + var i = info.ButtonID - 4000; + if (defaults.SelectionList != null && i >= 0 && i < defaults.SelectionList.Length) + { + defaults.SelectionList[i] = !defaults.SelectionList[i]; + } + } + if (info.ButtonID >= 5000 && info.ButtonID < 5000 + MaxEntries) + { + var i = info.ButtonID - 5000; + + defaults.CategorySelectionIndex = i; + var newg = new XmlAddGump(state.Mobile, defaults.StartingLoc, defaults.StartingMap, false, defaults.ShowExtension, 0, 0); + + state.Mobile.SendGump(newg); + + if (defaults.NameList[i] == null || defaults.NameList[i].Length == 0) + { + // if no string has been entered then just use the full categorized add gump + state.Mobile.CloseGump(); + state.Mobile.SendGump(new XmlCategorizedAddGump(state.Mobile, defaults.CurrentCategory, defaults.CurrentCategoryPage, i, newg)); + } + else + { + // use the XmlPartialCategorizedAddGump + state.Mobile.CloseGump(); + + //Type [] types = (Type[])XmlPartialCategorizedAddGump.Match(defs.NameList[i]).ToArray(typeof(Type)); + var types = XmlPartialCategorizedAddGump.Match(defaults.NameList[i]); + state.Mobile.SendGump(new XmlPartialCategorizedAddGump(state.Mobile, defaults.NameList[i], 0, types, true, i, newg)); + } + + return; + } + + break; + } + } + + Refresh(state.Mobile); + } + + private class XmlAddOptionsGump : Gump + { + public XmlAddOptionsGump(Mobile from) : base(0, 0) + { + // read the text entries for default values + var acct = from.Account as Account; + XmlSpawnerDefaults.DefaultEntry defs = null; + + if (acct != null) + { + defs = XmlSpawnerDefaults.GetDefaults(acct.ToString(), from.Name); + } + + if (defs == null) + { + return; + } + + Closable = true; + Draggable = true; + AddPage(0); + AddBackground(0, 0, 300, 130, 5054); + + AddLabel(20, 5, 0, "Options"); + // add the AddGumpX/Y entries + AddImageTiled(5, 30, 40, 21, 0xBBC); + AddTextEntry(5, 30, 40, 21, 0, 100, defs.AddGumpX.ToString()); + AddLabel(45, 30, 0x384, "AddGumpX"); + + AddImageTiled(135, 30, 40, 21, 0xBBC); + AddTextEntry(135, 30, 40, 21, 0, 101, defs.AddGumpY.ToString()); + AddLabel(175, 30, 0x384, "AddGumpY"); + + // add the SpawnerGumpX/Y entries + AddImageTiled(5, 55, 40, 21, 0xBBC); + AddTextEntry(5, 55, 40, 21, 0, 102, defs.SpawnerGumpX.ToString()); + AddLabel(45, 55, 0x384, "SpawnerGumpX"); + + AddImageTiled(135, 55, 40, 21, 0xBBC); + AddTextEntry(135, 55, 40, 21, 0, 103, defs.SpawnerGumpY.ToString()); + AddLabel(175, 55, 0x384, "SpawnerGumpY"); + + // add the FindGumpX/Y entries + AddImageTiled(5, 80, 40, 21, 0xBBC); + AddTextEntry(5, 80, 40, 21, 0, 104, defs.FindGumpX.ToString()); + AddLabel(45, 80, 0x384, "FindGumpX"); + + AddImageTiled(135, 80, 40, 21, 0xBBC); + AddTextEntry(135, 80, 40, 21, 0, 105, defs.FindGumpY.ToString()); + AddLabel(175, 80, 0x384, "FindGumpY"); + } + + public override void OnResponse(NetState state, RelayInfo info) + { + if (info == null || state?.Mobile == null) + { + return; + } + + // read the text entries for default values + var defs = XmlSpawnerDefaults.GetDefaults(state.Account.ToString(), state.Mobile.Name); + if (defs == null) + { + return; + } + + var tr = info.GetTextEntry(100); // AddGumpX + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defs.AddGumpX = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + tr = info.GetTextEntry(101); // AddGumpY info + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defs.AddGumpY = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + tr = info.GetTextEntry(102); // SpawnerGumpX info + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defs.SpawnerGumpX = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + tr = info.GetTextEntry(103); // SpawnerGumpY info + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defs.SpawnerGumpY = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + tr = info.GetTextEntry(104); // FindGumpX info + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defs.FindGumpX = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + tr = info.GetTextEntry(105); // FindGumpY info + if (tr?.Text != null && tr.Text.Length > 0) + { + try { defs.FindGumpY = int.Parse(tr.Text); } + catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } + } + } + } + + private class XmlAddConfirmDeleteGump : Gump + { + private readonly XmlSpawner LastSpawner; + + public XmlAddConfirmDeleteGump(XmlSpawner lastSpawner) : base(0, 0) + { + LastSpawner = lastSpawner; + Closable = false; + Draggable = true; + AddPage(0); + AddBackground(10, 200, 200, 130, 5054); + + AddLabel(20, 225, 33, "Delete Last Spawner?"); + AddRadio(35, 255, 9721, 0x86A, false, 1); // accept/yes radio + AddRadio(135, 255, 9721, 0x86A, true, 2); // decline/no radio + AddHtmlLocalized(72, 255, 200, 30, 1049016, 0x7fff); // Yes + AddHtmlLocalized(172, 255, 200, 30, 1049017, 0x7fff); // No + AddButton(80, 289, 2130, 2129, 3); // Okay button + } + + public override void OnResponse(NetState state, RelayInfo info) + { + if (info == null || state?.Mobile == null) + { + return; + } + + var radiostate = -1; + if (info.Switches.Length > 0) + { + radiostate = info.Switches[0]; + } + switch (info.ButtonID) + { + default: + { + if (radiostate == 1 && LastSpawner != null && !LastSpawner.Deleted) + { // accept + // delete it + LastSpawner.Delete(); + } + break; + } + } + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlCategorizedAddGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlCategorizedAddGump.cs new file mode 100644 index 000000000..9f2d863a2 --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlCategorizedAddGump.cs @@ -0,0 +1,477 @@ +using Server.Mobiles; +using Server.Network; +using System; +using System.Collections; +using System.IO; +using System.Xml; + +namespace Server.Gumps; + +public abstract class XmlAddCAGNode +{ + public abstract string Caption { get; } + public abstract void OnClick(Mobile from, int page, int index, Gump gump); +} + +public class XmlAddCAGObject : XmlAddCAGNode +{ + private readonly Type m_Type; + private readonly XmlAddCAGCategory m_Parent; + + public int ItemID { get; } + + public override string Caption => m_Type == null ? "bad type" : m_Type.Name; + + public override void OnClick(Mobile from, int page, int index, Gump gump) + { + if (m_Type == null) + { + from.SendMessage("That is an invalid type name."); + } + else + { + if (gump is XmlAddGump xmladdgump) + { + //Commands.Handle(from, String.Format("{0}Add {1}", Commands.CommandPrefix, m_Type.Name)); + if (xmladdgump.defs?.NameList != null && index >= 0 && index < xmladdgump.defs.NameList.Length) + { + xmladdgump.defs.NameList[index] = m_Type.Name; + XmlAddGump.Refresh(from, true); + } + from.SendGump(new XmlCategorizedAddGump(from, m_Parent, page, index, xmladdgump)); + } + else if (gump is XmlSpawnerGump spawnerGump) + { + var m_Spawner = spawnerGump.m_Spawner; + + if (m_Spawner != null) + { + var xg = m_Spawner.SpawnerGump; + + if (xg != null) + { + xg.Rentry = new XmlSpawnerGump.ReplacementEntry + { + Typename = m_Type.Name, + Index = index, + Color = 0x1436 + }; + + Timer.DelayCall(TimeSpan.Zero, XmlSpawnerGump.RefreshSpawnerGumps, from); + } + } + } + } + } + + public XmlAddCAGObject(XmlAddCAGCategory parent, XmlReader xml) + { + m_Parent = parent; + + if (xml.MoveToAttribute("type")) + { + m_Type = AssemblyHandler.FindTypeByFullName(xml.Value, false); + } + + if (xml.MoveToAttribute("gfx")) + { + ItemID = XmlConvert.ToInt32(xml.Value); + } + + if (xml.MoveToAttribute("hue")) + { + XmlConvert.ToInt32(xml.Value); + } + } +} + +public class XmlAddCAGCategory : XmlAddCAGNode +{ + private readonly string m_Title; + + public XmlAddCAGNode[] Nodes { get; } + + public XmlAddCAGCategory Parent { get; } + + public override string Caption => m_Title; + + public override void OnClick(Mobile from, int page, int index, Gump gump) + { + from.SendGump(new XmlCategorizedAddGump(from, this, 0, index, gump)); + } + + private XmlAddCAGCategory() + { + m_Title = "no data"; + Nodes = new XmlAddCAGNode[0]; + } + + public XmlAddCAGCategory(XmlAddCAGCategory parent, XmlReader xml) + { + Parent = parent; + + if (xml.MoveToAttribute("title")) + { + m_Title = xml.Value == "Add Menu" ? "XmlAdd Menu" : xml.Value; + } + else + { + m_Title = "empty"; + } + + if (m_Title == "Docked") + { + m_Title = "Docked 2"; + } + + if (xml.IsEmptyElement) + { + Nodes = new XmlAddCAGNode[0]; + } + else + { + var nodes = new ArrayList(); + + try + { + while (xml.Read() && xml.NodeType != XmlNodeType.EndElement) + { + + if (xml.NodeType == XmlNodeType.Element && xml.Name == "object") + { + nodes.Add(new XmlAddCAGObject(this, xml)); + } + else if (xml.NodeType == XmlNodeType.Element && xml.Name == "category") + { + if (!xml.IsEmptyElement) + { + nodes.Add(new XmlAddCAGCategory(this, xml)); + } + } + else + { + xml.Skip(); + } + } + } + catch (Exception ex) + { + Console.WriteLine("XmlCategorizedAddGump: Corrupted Data/objects.xml file detected. Not all XmlCAG objects loaded. {0}", ex); + } + + Nodes = (XmlAddCAGNode[])nodes.ToArray(typeof(XmlAddCAGNode)); + } + } + + private static XmlAddCAGCategory m_Root; + public static XmlAddCAGCategory Root => m_Root ?? (m_Root = Load("Data/objects.xml")); + + public static XmlAddCAGCategory Load(string path) + { + if (File.Exists(path)) + { + var xml = new XmlTextReader(path) + { + WhitespaceHandling = WhitespaceHandling.None + }; + + while (xml.Read()) + { + if (xml.Name == "category" && xml.NodeType == XmlNodeType.Element) + { + var cat = new XmlAddCAGCategory(null, xml); + + xml.Close(); + + return cat; + } + } + } + + return new XmlAddCAGCategory(); + } +} + +public class XmlCategorizedAddGump : Gump +{ + public static bool OldStyle = PropsConfig.OldStyle; + + public static readonly int GumpOffsetX = PropsConfig.GumpOffsetX; + public static readonly int GumpOffsetY = PropsConfig.GumpOffsetY; + + public static readonly int TextHue = PropsConfig.TextHue; + public static readonly int TextOffsetX = PropsConfig.TextOffsetX; + + public static readonly int OffsetGumpID = PropsConfig.OffsetGumpID; + public static readonly int HeaderGumpID = PropsConfig.HeaderGumpID; + public static readonly int EntryGumpID = PropsConfig.EntryGumpID; + public static readonly int BackGumpID = PropsConfig.BackGumpID; + public static readonly int SetGumpID = PropsConfig.SetGumpID; + + public static readonly int SetWidth = PropsConfig.SetWidth; + public static readonly int SetOffsetX = PropsConfig.SetOffsetX, SetOffsetY = PropsConfig.SetOffsetY /*+ (((EntryHeight - 20) / 2) / 2)*/; + public static readonly int SetButtonID1 = PropsConfig.SetButtonID1; + public static readonly int SetButtonID2 = PropsConfig.SetButtonID2; + + public static readonly int PrevWidth = PropsConfig.PrevWidth; + public static readonly int PrevOffsetX = PropsConfig.PrevOffsetX, PrevOffsetY = PropsConfig.PrevOffsetY /*+ (((EntryHeight - 20) / 2) / 2)*/; + public static readonly int PrevButtonID1 = PropsConfig.PrevButtonID1; + public static readonly int PrevButtonID2 = PropsConfig.PrevButtonID2; + + public static readonly int NextWidth = PropsConfig.NextWidth; + public static readonly int NextOffsetX = PropsConfig.NextOffsetX, NextOffsetY = PropsConfig.NextOffsetY /*+ (((EntryHeight - 20) / 2) / 2)*/; + public static readonly int NextButtonID1 = PropsConfig.NextButtonID1; + public static readonly int NextButtonID2 = PropsConfig.NextButtonID2; + + public static readonly int OffsetSize = PropsConfig.OffsetSize; + + public static readonly int EntryHeight = 24; + public static readonly int BorderSize = PropsConfig.BorderSize; + + private static readonly bool PrevLabel = false, NextLabel = false; + + private static readonly int PrevLabelOffsetX = PrevWidth + 1; + private const int PrevLabelOffsetY = 0; + + private const int NextLabelOffsetX = -29; + private const int NextLabelOffsetY = 0; + + private const int EntryWidth = 180; + private const int EntryCount = 15; + + private static readonly int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize; + + private static readonly int BackWidth = BorderSize + TotalWidth + BorderSize; + private readonly Mobile m_Owner; + private readonly XmlAddCAGCategory m_Category; + private int m_Page; + + private readonly int m_Index; + private readonly Gump m_Gump; + + public XmlCategorizedAddGump(Mobile owner, int index, Gump gump) : this(owner, XmlAddCAGCategory.Root, 0, index, gump) + { + } + + public XmlCategorizedAddGump(Mobile owner, XmlAddCAGCategory category, int page, int index, Gump gump) : base(GumpOffsetX, GumpOffsetY) + { + if (category == null) + { + category = XmlAddCAGCategory.Root; + page = 0; + } + + owner.CloseGump(); + + m_Owner = owner; + m_Category = category; + + m_Index = index; + m_Gump = gump; + + if (gump is XmlAddGump xmladdgump) + { + if (xmladdgump.defs != null) + { + xmladdgump.defs.CurrentCategory = category; + xmladdgump.defs.CurrentCategoryPage = page; + } + } + + Initialize(page); + } + + public void Initialize(int page) + { + m_Page = page; + + var nodes = m_Category.Nodes; + + var count = nodes.Length - page * EntryCount; + + if (count < 0) + { + count = 0; + } + else if (count > EntryCount) + { + count = EntryCount; + } + + var totalHeight = OffsetSize + (EntryHeight + OffsetSize) * (count + 1); + + AddPage(0); + + AddBackground(0, 0, BackWidth, BorderSize + totalHeight + BorderSize, BackGumpID); + AddImageTiled(BorderSize, BorderSize, TotalWidth - (OldStyle ? SetWidth + OffsetSize : 0), totalHeight, OffsetGumpID); + + var x = BorderSize + OffsetSize; + var y = BorderSize + OffsetSize; + + if (OldStyle) + { + AddImageTiled(x, y, TotalWidth - OffsetSize * 3 - SetWidth, EntryHeight, HeaderGumpID); + } + else + { + AddImageTiled(x, y, PrevWidth, EntryHeight, HeaderGumpID); + } + + if (m_Category.Parent != null) + { + AddButton(x + PrevOffsetX, y + PrevOffsetY, PrevButtonID1, PrevButtonID2, 1); + + if (PrevLabel) + { + AddLabel(x + PrevLabelOffsetX, y + PrevLabelOffsetY, TextHue, "Previous"); + } + } + + x += PrevWidth + OffsetSize; + + var emptyWidth = TotalWidth - PrevWidth * 2 - NextWidth - OffsetSize * 5 - (OldStyle ? SetWidth + OffsetSize : 0); + + if (!OldStyle) + { + AddImageTiled(x - (OldStyle ? OffsetSize : 0), y, emptyWidth + (OldStyle ? OffsetSize * 2 : 0), EntryHeight, EntryGumpID); + } + + AddHtml(x + TextOffsetX, y + (EntryHeight - 20) / 2, emptyWidth - TextOffsetX, EntryHeight, + $"
{m_Category.Caption}
"); + + x += emptyWidth + OffsetSize; + + 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, 2); + + if (PrevLabel) + { + AddLabel(x + PrevLabelOffsetX, y + PrevLabelOffsetY, TextHue, "Previous"); + } + } + + x += PrevWidth + OffsetSize; + + if (!OldStyle) + { + AddImageTiled(x, y, NextWidth, EntryHeight, HeaderGumpID); + } + + if ((page + 1) * EntryCount < nodes.Length) + { + AddButton(x + NextOffsetX, y + NextOffsetY, NextButtonID1, NextButtonID2, 3, GumpButtonType.Reply, 1); + + if (NextLabel) + { + AddLabel(x + NextLabelOffsetX, y + NextLabelOffsetY, TextHue, "Next"); + } + } + + for (int i = 0, index = page * EntryCount; i < EntryCount && index < nodes.Length; ++i, ++index) + { + x = BorderSize + OffsetSize; + y += EntryHeight + OffsetSize; + + var node = nodes[index]; + + AddImageTiled(x, y, EntryWidth, EntryHeight, EntryGumpID); + AddLabelCropped(x + TextOffsetX, y + (EntryHeight - 20) / 2, EntryWidth - TextOffsetX, EntryHeight, TextHue, node.Caption); + + x += EntryWidth + OffsetSize; + + if (SetGumpID != 0) + { + AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID); + } + + AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, i + 4); + + if (node is XmlAddCAGObject obj) + { + var itemID = obj.ItemID; + + var bounds = ItemBounds.Table[itemID]; + + if (itemID != 1 && bounds.Height < EntryHeight * 2) + { + if (bounds.Height < EntryHeight) + { + AddItem(x - OffsetSize - 22 - i % 2 * 44 - bounds.Width / 2 - bounds.X, y + EntryHeight / 2 - bounds.Height / 2 - bounds.Y, itemID); + } + else + { + AddItem(x - OffsetSize - 22 - i % 2 * 44 - bounds.Width / 2 - bounds.X, y + EntryHeight - 1 - bounds.Height - bounds.Y, itemID); + } + } + } + } + } + + public override void OnResponse(NetState state, RelayInfo info) + { + var from = m_Owner; + + switch (info.ButtonID) + { + case 0: // Closed + { + return; + } + case 1: // Up + { + if (m_Category.Parent != null) + { + var index = Array.IndexOf(m_Category.Parent.Nodes, m_Category) / EntryCount; + + if (index < 0) + { + index = 0; + } + + from.SendGump(new XmlCategorizedAddGump(from, m_Category.Parent, index, m_Index, m_Gump)); + } + + break; + } + case 2: // Previous + { + if (m_Page > 0) + { + from.SendGump(new XmlCategorizedAddGump(from, m_Category, m_Page - 1, m_Index, m_Gump)); + } + + break; + } + case 3: // Next + { + if ((m_Page + 1) * EntryCount < m_Category.Nodes.Length) + { + from.SendGump(new XmlCategorizedAddGump(from, m_Category, m_Page + 1, m_Index, m_Gump)); + } + + break; + } + default: + { + var index = m_Page * EntryCount + (info.ButtonID - 4); + + if (index >= 0 && index < m_Category.Nodes.Length) + { + m_Category.Nodes[index].OnClick(from, m_Page, m_Index, m_Gump); + } + + break; + } + } + } +} diff --git a/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlPartialCategorizedAddGump.cs b/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlPartialCategorizedAddGump.cs new file mode 100644 index 000000000..641714baf --- /dev/null +++ b/Projects/UOContent/Engines/XMLSpawner/XmlUtils/XmlPartialCategorizedAddGump.cs @@ -0,0 +1,266 @@ +using Server.Mobiles; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Server.Gumps; + +public class XmlPartialCategorizedAddGump : Gump +{ + private readonly string m_SearchString; + private readonly ArrayList m_SearchResults; + private readonly int m_Page; + private readonly Gump m_Gump; + private readonly int m_EntryIndex; + private readonly XmlSpawner m_Spawner; + + public XmlPartialCategorizedAddGump(Mobile from, string searchString, int page, ArrayList searchResults, bool explicitSearch, int entryindex, Gump gump) : base(50, 50) + { + if (gump is XmlSpawnerGump spawnerGump) + { + // keep track of the spawner for xmlspawnergumps + m_Spawner = spawnerGump.m_Spawner; + } + + // keep track of the gump + m_Gump = gump; + + + m_SearchString = searchString; + m_SearchResults = searchResults; + m_Page = page; + + m_EntryIndex = entryindex; + + from.CloseGump(); + + AddPage(0); + + AddBackground(0, 0, 420, 280, 5054); + + AddImageTiled(10, 10, 400, 20, 2624); + AddAlphaRegion(10, 10, 400, 20); + AddImageTiled(41, 11, 184, 18, 0xBBC); + AddImageTiled(42, 12, 182, 16, 2624); + AddAlphaRegion(42, 12, 182, 16); + + AddButton(10, 9, 4011, 4013, 1); + AddTextEntry(44, 10, 180, 20, 0x480, 0, searchString); + + AddHtmlLocalized(230, 10, 100, 20, 3010005, 0x7FFF); + + AddImageTiled(10, 40, 400, 200, 2624); + AddAlphaRegion(10, 40, 400, 200); + + if (searchResults.Count > 0) + { + for (var i = page * 10; i < (page + 1) * 10 && i < searchResults.Count; ++i) + { + var index = i % 10; + + var se = (SearchEntry)searchResults[i]; + + var labelstr = se.EntryType.Name; + + if (se.Parameters.Length > 0) + { + for (var j = 0; j < se.Parameters.Length; j++) + { + labelstr += $", {se.Parameters[j].Name}"; + } + } + + AddLabel(44, 39 + index * 20, 0x480, labelstr); + AddButton(10, 39 + index * 20, 4023, 4025, 4 + i); + } + } + else + { + AddLabel(15, 44, 0x480, explicitSearch ? "Nothing matched your search terms." : "No results to display."); + } + + AddImageTiled(10, 250, 400, 20, 2624); + AddAlphaRegion(10, 250, 400, 20); + + if (m_Page > 0) + { + AddButton(10, 249, 4014, 4016, 2); + } + else + { + AddImage(10, 249, 4014); + } + + AddHtmlLocalized(44, 250, 170, 20, 1061028, m_Page > 0 ? 0x7FFF : 0x5EF7); // Previous page + + if ((m_Page + 1) * 10 < searchResults.Count) + { + AddButton(210, 249, 4005, 4007, 3); + } + else + { + AddImage(210, 249, 4005); + } + + AddHtmlLocalized(244, 250, 170, 20, 1061027, (m_Page + 1) * 10 < searchResults.Count ? 0x7FFF : 0x5EF7); // Next page + } + + private static readonly Type typeofItem = typeof(Item), typeofMobile = typeof(Mobile); + + private class SearchEntry + { + public Type EntryType; + public ParameterInfo[] Parameters; + } + private static void Match(string match, IReadOnlyList types, IList results) + { + if (match.Length == 0) + { + return; + } + + match = match.ToLower(); + + for (var i = 0; i < types.Count; ++i) + { + var t = types[i]; + + if ((typeofMobile.IsAssignableFrom(t) || typeofItem.IsAssignableFrom(t)) && t.Name.ToLower().IndexOf(match) >= 0 && !results.Contains(t)) + { + var ctors = t.GetConstructors(); + + for (var j = 0; j < ctors.Length; ++j) + { + if (/*ctors[j].GetParameters().Length == 0 && */ ctors[j].IsDefined(typeof(ConstructibleAttribute), false)) + { + var s = new SearchEntry + { + EntryType = t, + Parameters = ctors[j].GetParameters() + }; + //results.Add(t); + results.Add(s); + //break; + } + } + } + } + } + + public static ArrayList Match(string match) + { + var results = new ArrayList(); + Type[] types; + + var asms = AssemblyHandler.Assemblies; + + for (var i = 0; i < asms.Length; ++i) + { + types = AssemblyHandler.GetTypeCache(asms[i]).Types; + Match(match, types, results); + } + + types = AssemblyHandler.GetTypeCache(Core.Assembly).Types; + Match(match, types, results); + + results.Sort(new TypeNameComparer()); + + return results; + } + + private class TypeNameComparer : IComparer + { + public int Compare(object x, object y) + { + var a = x as SearchEntry; + var b = y as SearchEntry; + + return a.EntryType.Name.CompareTo(b.EntryType.Name); + } + } + + + public override void OnResponse(Network.NetState sender, RelayInfo info) + { + var from = sender.Mobile; + + switch (info.ButtonID) + { + case 1: // Search + { + var te = info.GetTextEntry(0); + var match = te == null ? "" : te.Text.Trim(); + + if (match.Length < 3) + { + from.SendMessage("Invalid search string."); + from.SendGump(new XmlPartialCategorizedAddGump(from, match, m_Page, m_SearchResults, false, m_EntryIndex, m_Gump)); + } + else + { + from.SendGump(new XmlPartialCategorizedAddGump(from, match, 0, Match(match), true, m_EntryIndex, m_Gump)); + } + + break; + } + case 2: // Previous page + { + if (m_Page > 0) + { + from.SendGump(new XmlPartialCategorizedAddGump(from, m_SearchString, m_Page - 1, m_SearchResults, true, m_EntryIndex, m_Gump)); + } + + break; + } + case 3: // Next page + { + if ((m_Page + 1) * 10 < m_SearchResults.Count) + { + from.SendGump(new XmlPartialCategorizedAddGump(from, m_SearchString, m_Page + 1, m_SearchResults, true, m_EntryIndex, m_Gump)); + } + + break; + } + default: + { + var index = info.ButtonID - 4; + + if (index >= 0 && index < m_SearchResults.Count) + { + var type = ((SearchEntry)m_SearchResults[index]).EntryType; + + if (m_Gump is XmlAddGump mXmlAddGump && type != null) + { + if (mXmlAddGump.defs?.NameList != null && m_EntryIndex >= 0 && m_EntryIndex < mXmlAddGump.defs.NameList.Length) + { + mXmlAddGump.defs.NameList[m_EntryIndex] = type.Name; + XmlAddGump.Refresh(from, true); + } + } + else if (m_Spawner != null && type != null) + { + var xg = m_Spawner.SpawnerGump; + + if (xg != null) + { + + xg.Rentry = new XmlSpawnerGump.ReplacementEntry + { + Typename = type.Name, + Index = m_EntryIndex, + Color = 0x1436 + }; + + Timer.DelayCall(TimeSpan.Zero, XmlSpawnerGump.RefreshSpawnerGumps, from); + //from.CloseGump(); + //from.SendGump(new XmlSpawnerGump(xg.m_Spawner, xg.X, xg.Y, xg.m_ShowGump, xg.xoffset, xg.page, xg.Rentry)); + } + } + } + + break; + } + } + } +}