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) { IPooledEnumerable ie = item.GetMobilesInRange(range); foreach (Mobile p in ie) { if (p.Player && p.AccessLevel == AccessLevel.Player) { nplayers++; } } ie.Free(); } else if (refobject is Mobile mobile) { IPooledEnumerable ie = mobile.GetMobilesInRange(range); foreach (Mobile p in ie) { if (p.Player && p.AccessLevel == AccessLevel.Player) { nplayers++; } } ie.Free(); } 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) { IPooledEnumerable ie = item.GetMobilesInRange(range); foreach (Mobile p in ie) { if (p.Player && p.AccessLevel == AccessLevel.Player) { nplayers++; } } ie.Free(); } else if (o is Mobile mobile) { IPooledEnumerable ie = mobile.GetMobilesInRange(range); foreach (Mobile p in ie) { if (p.Player && p.AccessLevel == AccessLevel.Player) { nplayers++; } } ie.Free(); } 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; } }