using ACE.Entity; using ACE.Entity.Enum; using ACE.Server.Entity; using ACE.Server.Managers; using ACE.Server.Network; using ACE.Server.WorldObjects; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text.RegularExpressions; namespace ACE.Server.Command { /// /// Command handler sanity preserving parameter parsing /// public class CommandParameterHelpers { /// /// Types of parameter values /// public enum ACECommandParameterType { /// /// don't use this one /// Invalid, /// /// normal coordinates, example: 37.3s,67w /// output is of type ACE.Entity.Position /// Location, /// /// a character name of an online player, example: the fat bastard /// output is of type ACE.Server.WorldObjects.Player /// only one of OnlinePlayerName or OnlinePlayerNameOrIid or PlayerName parameter can be used for one command /// must be the first parameter /// OnlinePlayerName, /// /// a character name of an online player, example: the fat bastard or a decimal iid, example: 1342177281 /// output is of type ACE.Server.WorldObjects.Player /// only one of OnlinePlayerName or OnlinePlayerNameOrIid or PlayerName parameter can be used for one command /// must be the first parameter /// OnlinePlayerNameOrIid, /// /// a decimal or hexadecimal iid of an online player, examples: 1342177292 or 0x5000000C /// output is of type ACE.Server.WorldObjects.Player /// OnlinePlayerIid, /// /// a character name, example: the fat bastard /// output is of type string /// only one of OnlinePlayerName or OnlinePlayerNameOrIid or PlayerName parameter can be used for one command /// must be the first parameter /// PlayerName, /// /// a url, example: http://someserver.net:4321/?get=blah /// output is of type System.Uri /// Uri, /// /// a number, example: 01231242 /// output is of type ulong: 1231242 /// ULong, /// /// a number, example: -01231242 /// output is of type long: -1231242 /// Long, /// /// a number, example: 01231242 /// output is of type long: 1231242 /// PositiveLong, /// /// some text enclosed in double quotes, example: "the problem is solved" /// Note: To accept this kind of parameter IncludeRaw must be true for the command handler attribute decoration and RawIncluded argument must be true for the call to ResolveACEParameters /// DoubleQuoteEnclosedText, /// /// some text preceeded by a comma, example (the part after char): /hug my girl, char, the reasons are plenty! /// Note: To accept this kind of parameter IncludeRaw must be true for the command handler attribute decoration and RawIncluded argument must be true for the call to ResolveACEParameters /// limit 1 per command, may not include commas /// CommaPrefixedText, /// /// a word, example: boat /// output is of type string /// word may be one or more of case insensitive a-z, 0-9, and underscore /// SimpleWord, /// /// an element of an enum /// output is of enum type you supply as PossibleValues /// set the PossibleValues object to the type of enum /// Enum, } /// /// A player supplied parameter /// public class ACECommandParameter { /// /// The type of the parameter value /// public ACECommandParameterType Type { get; set; } = ACECommandParameterType.Invalid; /// /// The value to use upon missing or invalid supplied parameter /// public object DefaultValue { get; set; } = null; /// /// If this parameter is required /// public bool Required { get; set; } = false; /// /// The resultant parsed Value (or the default value) /// public object Value { get; set; } = null; public Position AsPosition => (Position)Value; public Player AsPlayer => (Player)Value; public ulong AsULong => (ulong)Value; public long AsLong => (long)Value; public long AsInt => (int)Value; public string AsString => (string)Value; public Uri AsUri => (Uri)Value; /// /// The parameter either wasn't supplied or was invalid (doesn't parse, player doesn't exist, etc.) /// public bool Defaulted { get; set; } = true; /// /// The broadcast message to send to the session when the parameter is required and didn't parse or wasn't supplied. /// public string ErrorMessage { get; set; } = null; /// /// Automatically assigned during GetParameters procedure /// public int ParameterNo { get; set; } = -1; /// /// if the type is enum set this to the enum that the parameter should be part of /// public Type PossibleValues { get; set; } = null; } /// /// Resolve the parameters supplied by the player into usable values. /// /// the session of the player who sent the command /// the collection of parameters supplied by the default parameter parser /// the resolution details for every parameter /// whether or not the raw unparsed command line minus the command name was included as the first parameter /// the parameters were successfully resolved or not public static bool ResolveACEParameters(Session session, IEnumerable aceParsedParameters, IEnumerable parameters, bool rawIncluded = false) { string parameterBlob = ""; if (rawIncluded) { parameterBlob = aceParsedParameters.First(); } else { parameterBlob = aceParsedParameters.Count() > 0 ? aceParsedParameters.Aggregate((a, b) => a + " " + b).Trim(new char[] { ' ', ',' }) : string.Empty; } int commaCount = parameterBlob.Count(x => x == ','); List acps = parameters.ToList(); for (int i = acps.Count - 1; i > -1; i--) { ACECommandParameter acp = acps[i]; acp.ParameterNo = i + 1; if (parameterBlob.Length > 0) { try { switch (acp.Type) { case ACECommandParameterType.PositiveLong: Match match4 = Regex.Match(parameterBlob, @"(-?\d+)$", RegexOptions.IgnoreCase); if (match4.Success) { if (!long.TryParse(match4.Groups[1].Value, out long val)) { return false; } if (val <= 0) { return false; } acp.Value = val; acp.Defaulted = false; parameterBlob = (match4.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match4.Groups[1].Index).Trim(new char[] { ' ' }); } break; case ACECommandParameterType.Long: Match match3 = Regex.Match(parameterBlob, @"(-?\d+)$", RegexOptions.IgnoreCase); if (match3.Success) { if (!long.TryParse(match3.Groups[1].Value, out long val)) { return false; } acp.Value = val; acp.Defaulted = false; parameterBlob = (match3.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match3.Groups[1].Index).Trim(new char[] { ' ', ',' }); } break; case ACECommandParameterType.ULong: Match match2 = Regex.Match(parameterBlob, @"(-?\d+)$", RegexOptions.IgnoreCase); if (match2.Success) { if (!ulong.TryParse(match2.Groups[1].Value, out ulong val)) { return false; } acp.Value = val; acp.Defaulted = false; parameterBlob = (match2.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match2.Groups[1].Index).Trim(new char[] { ' ', ',' }); } break; case ACECommandParameterType.Location: Position position = null; Match match = Regex.Match(parameterBlob, @"([\d\.]+[ns])[^\d\.]*([\d\.]+[ew])$", RegexOptions.IgnoreCase); if (match.Success) { string ns = match.Groups[1].Value; string ew = match.Groups[2].Value; if (!TryParsePosition(new string[] { ns, ew }, out string errorMessage, out position)) { if (session != null) { ChatPacket.SendServerMessage(session, errorMessage, ChatMessageType.Broadcast); } else { Console.WriteLine(errorMessage); } return false; } else { acp.Value = position; acp.Defaulted = false; int coordsStartPos = Math.Min(match.Groups[1].Index, match.Groups[2].Index); parameterBlob = (coordsStartPos == 0) ? string.Empty : parameterBlob.Substring(0, coordsStartPos).Trim(new char[] { ' ', ',' }); } } break; case ACECommandParameterType.OnlinePlayerName: if (i != 0) { throw new Exception("Player parameter must be the first parameter, since it can contain spaces."); } parameterBlob = parameterBlob.TrimEnd(new char[] { ' ', ',' }); Player targetPlayer = PlayerManager.GetOnlinePlayer(parameterBlob); if (targetPlayer == null) { string errorMsg = $"Unable to find player {parameterBlob}"; if (session != null) { ChatPacket.SendServerMessage(session, errorMsg, ChatMessageType.Broadcast); } else { Console.WriteLine(errorMsg); } return false; } else { acp.Value = targetPlayer; acp.Defaulted = false; } break; case ACECommandParameterType.OnlinePlayerNameOrIid: if (i != 0) { throw new Exception("Player parameter must be the first parameter, since it can contain spaces."); } if (!parameterBlob.Contains(' ')) { if (uint.TryParse(parameterBlob, out uint iid)) { Player targetPlayer2 = PlayerManager.GetOnlinePlayer(iid); if (targetPlayer2 == null) { string logMsg = $"Unable to find player with iid {iid}"; if (session != null) { ChatPacket.SendServerMessage(session, logMsg, ChatMessageType.Broadcast); } else { Console.WriteLine(logMsg); } return false; } else { acp.Value = targetPlayer2; acp.Defaulted = false; break; } } } Player targetPlayer3 = PlayerManager.GetOnlinePlayer(parameterBlob); if (targetPlayer3 == null) { string logMsg = $"Unable to find player {parameterBlob}"; if (session != null) { ChatPacket.SendServerMessage(session, logMsg, ChatMessageType.Broadcast); } else { Console.WriteLine(logMsg); } return false; } else { acp.Value = targetPlayer3; acp.Defaulted = false; } break; case ACECommandParameterType.OnlinePlayerIid: Match matcha5 = Regex.Match(parameterBlob, /*((i == 0) ? "" : @"\s+") +*/ @"(\d{10})$|(0x[0-9a-f]{8})$", RegexOptions.IgnoreCase); if (matcha5.Success) { string strIid = ""; if (matcha5.Groups[2].Success) { strIid = matcha5.Groups[2].Value; } else if (matcha5.Groups[1].Success) { strIid = matcha5.Groups[1].Value; } try { uint iid = 0; if (strIid.StartsWith("0x")) { iid = Convert.ToUInt32(strIid, 16); } else { iid = uint.Parse(strIid); } Player targetPlayer2 = PlayerManager.GetOnlinePlayer(iid); if (targetPlayer2 == null) { string logMsg = $"Unable to find player with iid {strIid}"; if (session != null) { ChatPacket.SendServerMessage(session, logMsg, ChatMessageType.Broadcast); } else { Console.WriteLine(logMsg); } return false; } else { acp.Value = targetPlayer2; acp.Defaulted = false; parameterBlob = (matcha5.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, matcha5.Groups[1].Index).Trim(new char[] { ' ', ',' }); } } catch (Exception) { string errorMsg = $"Unable to parse {strIid} into a player iid"; if (session != null) { ChatPacket.SendServerMessage(session, errorMsg, ChatMessageType.Broadcast); } else { Console.WriteLine(errorMsg); } return false; } } break; case ACECommandParameterType.PlayerName: if (i != 0) { throw new Exception("Player name parameter must be the first parameter, since it can contain spaces."); } parameterBlob = parameterBlob.TrimEnd(new char[] { ' ', ',' }); if (string.IsNullOrWhiteSpace(parameterBlob)) { break; } else { acp.Value = parameterBlob; acp.Defaulted = false; } break; case ACECommandParameterType.Uri: Match match5 = Regex.Match(parameterBlob, @"(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))$", RegexOptions.IgnoreCase); if (match5.Success) { string strUri = match5.Groups[1].Value; try { Uri url = new Uri(strUri); acp.Value = url; acp.Defaulted = false; parameterBlob = (match5.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match5.Groups[1].Index).Trim(new char[] { ' ', ',' }); } catch (Exception) { return false; } } break; case ACECommandParameterType.DoubleQuoteEnclosedText: Match match6 = Regex.Match(parameterBlob.TrimEnd(), @"(\"".*\"")$", RegexOptions.IgnoreCase); if (match6.Success) { string txt = match6.Groups[1].Value; try { acp.Value = txt.Trim('"'); acp.Defaulted = false; parameterBlob = (match6.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match6.Groups[1].Index).Trim(new char[] { ' ', ',' }); } catch (Exception) { return false; } } break; case ACECommandParameterType.CommaPrefixedText: if (i == 0) { throw new Exception("this parameter type is not appropriate as the first parameter"); } if (i == acps.Count - 1 && !acp.Required && commaCount < acps.Count - 1) { break; } Match match7 = Regex.Match(parameterBlob.TrimEnd(), @"\,\s*([^,]*)$", RegexOptions.IgnoreCase); if (match7.Success) { string txt = match7.Groups[1].Value; try { acp.Value = txt.TrimStart(new char[] { ' ', ',' }); acp.Defaulted = false; parameterBlob = (match7.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match7.Groups[1].Index).Trim(new char[] { ' ', ',' }); } catch (Exception) { return false; } } break; case ACECommandParameterType.SimpleWord: Match match8 = Regex.Match(parameterBlob.TrimEnd(), @"([a-zA-Z1-9_]+)\s*$", RegexOptions.IgnoreCase); if (match8.Success) { string txt = match8.Groups[1].Value; try { acp.Value = txt.TrimStart(' '); acp.Defaulted = false; parameterBlob = (match8.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match8.Groups[1].Index).Trim(new char[] { ' ', ',' }); } catch (Exception) { return false; } } break; case ACECommandParameterType.Enum: if (acp.PossibleValues == null) { throw new Exception("The enum parameter type must be accompanied by the PossibleValues"); } if (!acp.PossibleValues.IsEnum) { throw new Exception("PossibleValues must be an enum type"); } Match match9 = Regex.Match(parameterBlob.TrimEnd(), @"([a-zA-Z1-9_]+)\s*$", RegexOptions.IgnoreCase); if (match9.Success) { string txt = match9.Groups[1].Value; try { txt = txt.Trim(new char[] { ' ', ',' }); Array etvs = Enum.GetValues(acp.PossibleValues); foreach (object etv in etvs) { if (etv.ToString().ToLower() == txt.ToLower()) { acp.Value = etv; acp.Defaulted = false; parameterBlob = (match9.Groups[1].Index == 0) ? string.Empty : parameterBlob.Substring(0, match9.Groups[1].Index).Trim(new char[] { ' ' }); break; } } } catch (Exception) { return false; } } break; } } catch { return false; } } if (acp.Defaulted) { acp.Value = acp.DefaultValue; } if (acp.Required && acp.Defaulted) { if (!string.IsNullOrWhiteSpace(acp.ErrorMessage)) { if (session != null) { ChatPacket.SendServerMessage(session, acp.ErrorMessage, ChatMessageType.Broadcast); } else { Console.WriteLine(acp.ErrorMessage); } } return false; } } return true; } /// /// Try to parse a player supplied coordinate into a position /// /// parameter array containing 2 contiguous elements: first is north/south, second is east/west /// the problem encountered while trying to parse /// the resultant ACE.Entity.Position /// the first zero based element index of the 2 contiguous elements in the parameter array /// the parsing was successful or not public static bool TryParsePosition(string[] parameters, out string errorMessage, out Position position, int startingElement = 0) { errorMessage = string.Empty; position = null; if (parameters.Length - startingElement - 1 < 1) { errorMessage = "not enough parameters"; return false; } string northSouth = parameters[startingElement].ToLower().Replace(",", "").Trim(); string eastWest = parameters[startingElement + 1].ToLower().Replace(",", "").Trim(); if (!northSouth.EndsWith("n") && !northSouth.EndsWith("s")) { errorMessage = "Missing n or s indicator on first parameter"; return false; } if (!eastWest.EndsWith("e") && !eastWest.EndsWith("w")) { errorMessage = "Missing e or w indicator on second parameter"; return false; } if (!float.TryParse(northSouth.Substring(0, northSouth.Length - 1), out float coordNS)) { errorMessage = "North/South coordinate is not a valid number."; return false; } if (!float.TryParse(eastWest.Substring(0, eastWest.Length - 1), out float coordEW)) { errorMessage = "East/West coordinate is not a valid number."; return false; } if (northSouth.EndsWith("s")) { coordNS *= -1.0f; } if (eastWest.EndsWith("w")) { coordEW *= -1.0f; } try { position = new Position(new Vector2(coordEW, coordNS)); position.AdjustMapCoords(); } catch (Exception e) { Console.WriteLine(e); errorMessage = $"There was a problem with that location (bad coordinates?)."; return false; } return true; } } }