/* TShock, a server mod for Terraria Copyright (C) 2011-2017 Nyx Studios (fka. The TShock Team) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using Microsoft.Xna.Framework; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Threading; using System.Timers; using OTAPI.Tile; using Terraria; using Terraria.DataStructures; using Terraria.ID; using Terraria.Localization; using TShockAPI.DB; using TShockAPI.Hooks; using TShockAPI.Net; using Timer = System.Timers.Timer; namespace TShockAPI { /// /// Bitflags used with the method /// [Flags] public enum DisableFlags { /// /// Disable the player and leave no messages /// None, /// /// Write the Disable message to the console /// WriteToConsole, /// /// Write the Disable message to the log /// WriteToLog, /// /// Equivalent to WriteToConsole | WriteToLog /// WriteToLogAndConsole } public class TSPlayer { /// /// This represents the server as a player. /// public static readonly TSServerPlayer Server = new TSServerPlayer(); /// /// This player represents all the players. /// public static readonly TSPlayer All = new TSPlayer("All"); /// /// The amount of tiles that the player has killed in the last second. /// public int TileKillThreshold { get; set; } /// /// The amount of tiles the player has placed in the last second. /// public int TilePlaceThreshold { get; set; } /// /// The amount of liquid (in tiles) that the player has placed in the last second. /// public int TileLiquidThreshold { get; set; } /// /// The amount of tiles that the player has painted in the last second. /// public int PaintThreshold { get; set; } /// /// The number of projectiles created by the player in the last second. /// public int ProjectileThreshold { get; set; } /// /// The number of HealOtherPlayer packets sent by the player in the last second. /// public int HealOtherThreshold { get; set; } /// /// A timer to keep track of whether or not the player has recently thrown an explosive /// public int RecentFuse = 0; /// /// Whether to ignore packets that are SSC-relevant. /// public bool IgnoreSSCPackets { get; set; } /// /// A system to delay Remembered Position Teleports a few seconds /// public int RPPending = 0; public int sX = -1; public int sY = -1; /// /// A queue of tiles destroyed by the player for reverting. /// public Dictionary TilesDestroyed { get; protected set; } /// /// A queue of tiles placed by the player for reverting. /// public Dictionary TilesCreated { get; protected set; } /// /// The player's group. /// public Group Group { get { if (tempGroup != null) return tempGroup; return group; } set { group = value; } } /// /// The player's temporary group. This overrides the user's actual group. /// public Group tempGroup = null; public Timer tempGroupTimer; private Group group = null; public bool ReceivedInfo { get; set; } /// /// The players index in the player array( Main.players[] ). /// public int Index { get; protected set; } /// /// The last time the player changed their team or pvp status. /// public DateTime LastPvPTeamChange; /// /// Temp points for use in regions and other plugins. /// public Point[] TempPoints = new Point[2]; /// /// Whether the player is waiting to place/break a tile to set as a temp point. /// public int AwaitingTempPoint { get; set; } /// /// A list of command callbacks indexed by the command they need to do. /// public Dictionary> AwaitingResponse; public bool AwaitingName { get; set; } public string[] AwaitingNameParameters { get; set; } /// /// The last time a player broke a grief check. /// public DateTime LastThreat { get; set; } public bool InitSpawn; /// /// Whether the player should see logs. /// public bool DisplayLogs = true; /// /// The last player that the player whispered with (to or from). /// public TSPlayer LastWhisper; /// /// The number of unsuccessful login attempts. /// public int LoginAttempts { get; set; } /// /// Unused. /// public Vector2 TeleportCoords = new Vector2(-1, -1); /// /// The player's last known position from PlayerUpdate packet. /// public Vector2 LastNetPosition = Vector2.Zero; /// /// User object associated with the player. /// Set when the player logs in. /// public User User { get; set; } /// /// Whether the player performed a valid login attempt (i.e. entered valid user name and password) but is still blocked /// from logging in because of SSI. /// public bool LoginFailsBySsi { get; set; } /// /// Whether the player is logged in or not. /// public bool IsLoggedIn; /// /// Whether the player has sent their whole inventory to the server while connecting. /// public bool HasSentInventory { get; set; } /// /// Whether the player has been nagged about logging in. /// public bool HasBeenNaggedAboutLoggingIn; /// /// Whether other players can teleport to the player. /// public bool TPAllow = true; /// /// Whether the player is muted or not. /// public bool mute; private Player FakePlayer; public bool RequestedSection; /// /// The player's respawn timer. /// public int RespawnTimer; /// /// Whether the player is dead or not. /// public bool Dead; public string Country = "??"; /// /// The players difficulty( normal[softcore], mediumcore, hardcore ). /// public int Difficulty; private string CacheIP; public string IgnoreActionsForInventory = "none"; public string IgnoreActionsForCheating = "none"; public string IgnoreActionsForDisabledArmor = "none"; public bool IgnoreActionsForClearingTrashCan; /// /// The player's server side inventory data. /// public PlayerData PlayerData; /// /// Whether the player needs to specify a password upon connection( either server or user account ). /// public bool RequiresPassword; public bool SilentKickInProgress; public bool SilentJoinInProgress; /// /// A list of points where ice tiles have been placed. /// public List IceTiles; /// /// Unused, can be removed. /// public long RPm = 1; /// /// World protection message cool down. /// public long WPm = 1; /// /// Spawn protection message cool down. /// public long SPm = 1; /// /// Permission to build message cool down. /// public long BPm = 1; /// /// The time in ms when the player has logged in. /// public long LoginMS; /// /// Whether the player has been harrassed about logging in due to server side inventory or forced login. /// public bool LoginHarassed = false; /// /// Player cant die, unless onehit /// public bool GodMode = false; /// /// Players controls are inverted if using SSC /// public bool Confused = false; /// /// The last projectile type this player tried to kill. /// public int LastKilledProjectile = 0; /// /// The current region this player is in, or null if none. /// public Region CurrentRegion = null; /// /// Contains data stored by plugins /// protected ConcurrentDictionary data = new ConcurrentDictionary(); /// /// Whether the player is a real, human, player on the server. /// public bool RealPlayer { get { return Index >= 0 && Index < Main.maxNetPlayers && Main.player[Index] != null; } } /// /// Checks if the player is active and not pending termination. /// public bool ConnectionAlive { get { return RealPlayer && (Netplay.Clients[Index] != null && Netplay.Clients[Index].IsActive && !Netplay.Clients[Index].PendingTermination); } } /// /// Gets the item that the player is currently holding. /// public Item SelectedItem { get { return TPlayer.inventory[TPlayer.selectedItem]; } } /// /// Gets the player's Client State. /// public int State { get { return Netplay.Clients[Index].State; } set { Netplay.Clients[Index].State = value; } } /// /// Gets the player's UUID. /// public string UUID { get { return RealPlayer ? Netplay.Clients[Index].ClientUUID : ""; } } /// /// Gets the player's IP. /// public string IP { get { if (string.IsNullOrEmpty(CacheIP)) return CacheIP = RealPlayer ? (Netplay.Clients[Index].Socket.IsConnected() ? TShock.Utils.GetRealIP(Netplay.Clients[Index].Socket.GetRemoteAddress().ToString()) : "") : ""; else return CacheIP; } } /// /// Gets the player's accessories. /// public IEnumerable Accessories { get { for (int i = 3; i < 8; i++) yield return TPlayer.armor[i]; } } /// /// Saves the player's inventory to SSC /// /// bool - True/false if it saved successfully public bool SaveServerCharacter() { if (!Main.ServerSideCharacter) { return false; } try { if (HasPermission(Permissions.bypassssc)) { TShock.Log.ConsoleInfo("Skipping SSC Backup for " + User.Name); // Debug Code return true; } PlayerData.CopyCharacter(this); TShock.CharacterDB.InsertPlayerData(this); return true; } catch (Exception e) { TShock.Log.Error(e.Message); return false; } } /// /// Sends the players server side character to client /// /// bool - True/false if it saved successfully public bool SendServerCharacter() { if (!Main.ServerSideCharacter) { return false; } try { PlayerData.RestoreCharacter(this); return true; } catch (Exception e) { TShock.Log.Error(e.Message); return false; } } /// /// Gets the Terraria Player object associated with the player. /// public Player TPlayer { get { return FakePlayer ?? Main.player[Index]; } } /// /// Gets the player's name. /// public string Name { get { return TPlayer.name; } } /// /// Gets the player's active state. /// public bool Active { get { return TPlayer != null && TPlayer.active; } } /// /// Gets the player's team. /// public int Team { get { return TPlayer.team; } } /// /// Gets the player's X coordinate. /// public float X { get { return RealPlayer ? TPlayer.position.X : Main.spawnTileX*16; } } /// /// Gets the player's Y coordinate. /// public float Y { get { return RealPlayer ? TPlayer.position.Y : Main.spawnTileY*16; } } /// /// Player X coordinate divided by 16. Supposed X world coordinate. /// public int TileX { get { return (int) (X/16); } } /// /// Player Y cooridnate divided by 16. Supposed Y world coordinate. /// public int TileY { get { return (int) (Y/16); } } /// /// Unused. /// public bool TpLock; /// /// Checks if the player has any inventory slots available. /// public bool InventorySlotAvailable { get { bool flag = false; if (RealPlayer) { for (int i = 0; i < 50; i++) //51 is trash can, 52-55 is coins, 56-59 is ammo { if (TPlayer.inventory[i] == null || !TPlayer.inventory[i].active || TPlayer.inventory[i].Name == "") { flag = true; break; } } } return flag; } } /// /// This contains the character data a player has when they join the server. /// public PlayerData DataWhenJoined { get; set; } /// /// Determines whether the player's storage contains the given key. /// /// Key to test. /// public bool ContainsData(string key) { return data.ContainsKey(key); } /// /// Returns the stored object associated with the given key. /// /// Type of the object being retrieved. /// Key with which to access the object. /// The stored object, or default(T) if not found. public T GetData(string key) { object obj; if (!data.TryGetValue(key, out obj)) { return default(T); } return (T)obj; } /// /// Stores an object on this player, accessible with the given key. /// /// Type of the object being stored. /// Key with which to access the object. /// Object to store. public void SetData(string key, T value) { if (!data.TryAdd(key, value)) { data.TryUpdate(key, value, data[key]); } } /// /// Removes the stored object associated with the given key. /// /// Key with which to access the object. /// The removed object. public object RemoveData(string key) { object rem; if (data.TryRemove(key, out rem)) { return rem; } return null; } /// /// Logs the player out of an account. /// public void Logout() { PlayerHooks.OnPlayerLogout(this); if (Main.ServerSideCharacter) { IgnoreActionsForInventory = $"Server side characters is enabled! Please {Commands.Specifier}register or {Commands.Specifier}login to play!"; if (!IgnoreActionsForClearingTrashCan && (!Dead || TPlayer.difficulty != 2)) { PlayerData.CopyCharacter(this); TShock.CharacterDB.InsertPlayerData(this); } } PlayerData = new PlayerData(this); Group = TShock.Groups.GetGroupByName(TShock.Config.DefaultGuestGroupName); tempGroup = null; if (tempGroupTimer != null) { tempGroupTimer.Stop(); } User = null; IsLoggedIn = false; } /// /// Initializes a new instance of the class. /// /// The player's index in the. public TSPlayer(int index) { TilesDestroyed = new Dictionary(); TilesCreated = new Dictionary(); Index = index; Group = Group.DefaultGroup; IceTiles = new List(); AwaitingResponse = new Dictionary>(); } /// /// Initializes a new instance of the class. /// /// The player's name. protected TSPlayer(String playerName) { TilesDestroyed = new Dictionary(); TilesCreated = new Dictionary(); Index = -1; FakePlayer = new Player {name = playerName, whoAmI = -1}; Group = Group.DefaultGroup; AwaitingResponse = new Dictionary>(); } /// /// Disconnects the player from the server. /// /// The reason why the player was disconnected. public virtual void Disconnect(string reason) { SendData(PacketTypes.Disconnect, reason); } /// /// Fired when the player's temporary group access expires. /// /// /// public void TempGroupTimerElapsed(object sender, ElapsedEventArgs args) { SendWarningMessage("Your temporary group access has expired."); tempGroup = null; if (sender != null) { ((Timer)sender).Stop(); } } /// /// Teleports the player to the given coordinates in the world. /// /// The X coordinate. /// The Y coordinate. /// The teleportation style. /// True or false. public bool Teleport(float x, float y, byte style = 1) { if (x > Main.rightWorld - 992) { x = Main.rightWorld - 992; } if (x < 992) { x = 992; } if (y > Main.bottomWorld - 992) { y = Main.bottomWorld - 992; } if (y < 992) { y = 992; } SendTileSquare((int) (x/16), (int) (y/16), 15); TPlayer.Teleport(new Vector2(x, y), style); NetMessage.SendData((int)PacketTypes.Teleport, -1, -1, NetworkText.FromLiteral(""), 0, TPlayer.whoAmI, x, y, style); return true; } /// /// Heals the player. /// /// Heal health amount. public void Heal(int health = 600) { NetMessage.SendData((int)PacketTypes.PlayerHealOther, -1, -1, NetworkText.FromLiteral(""), this.TPlayer.whoAmI, health); } /// /// Spawns the player at his spawn point. /// public void Spawn() { if (this.sX > 0 && this.sY > 0) { Spawn(this.sX, this.sY); } else { Spawn(TPlayer.SpawnX, TPlayer.SpawnY); } } /// /// Spawns the player at the given coordinates. /// /// The X coordinate. /// The Y coordinate. public void Spawn(int tilex, int tiley) { using (var ms = new MemoryStream()) { var msg = new SpawnMsg { PlayerIndex = (byte) Index, TileX = (short)tilex, TileY = (short)tiley }; msg.PackFull(ms); SendRawData(ms.ToArray()); } } /// /// Removes the projectile with the given index and owner. /// /// The projectile's index. /// The projectile's owner. public void RemoveProjectile(int index, int owner) { using (var ms = new MemoryStream()) { var msg = new ProjectileRemoveMsg { Index = (short) index, Owner = (byte) owner }; msg.PackFull(ms); SendRawData(ms.ToArray()); } } public virtual bool SendTileSquare(int x, int y, int size = 10) { try { int num = (size - 1)/2; int m_x = 0; int m_y = 0; if (x - num < 0) { m_x = 0; } else { m_x = x - num; } if (y - num < 0) { m_y = 0; } else { m_y = y - num; } if (m_x + size > Main.maxTilesX) { m_x = Main.maxTilesX - size; } if (m_y + size > Main.maxTilesY) { m_y = Main.maxTilesY - size; } SendData(PacketTypes.TileSendSquare, "", size, m_x, m_y); return true; } catch (IndexOutOfRangeException) { // This is expected if square exceeds array. } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Gives an item to the player. Includes banned item spawn prevention to check if the player can spawn the item. /// /// /// /// /// /// /// /// True or false, depending if the item passed the check or not. public bool GiveItemCheck(int type, string name, int width, int height, int stack, int prefix = 0) { if ((TShock.Itembans.ItemIsBanned(name) && TShock.Config.PreventBannedItemSpawn) && (TShock.Itembans.ItemIsBanned(name, this) || !TShock.Config.AllowAllowedGroupsToSpawnBannedItems)) return false; GiveItem(type,name,width,height,stack,prefix); return true; } /// /// Gives an item to the player. /// /// The item's netID. /// The tiem's name. /// The item's width. /// The item's height. /// The item's stack. /// The item's prefix. public virtual void GiveItem(int type, string name, int width, int height, int stack, int prefix = 0) { int itemid = Item.NewItem((int) X, (int) Y, width, height, type, stack, true, prefix, true); // This is for special pickaxe/hammers/swords etc Main.item[itemid].netDefaults(type); // The set default overrides the wet and stack set by NewItem Main.item[itemid].wet = Collision.WetCollision(Main.item[itemid].position, Main.item[itemid].width, Main.item[itemid].height); Main.item[itemid].stack = stack; Main.item[itemid].owner = Index; Main.item[itemid].prefix = (byte) prefix; Main.item[itemid].noGrabDelay = 1; Main.item[itemid].velocity = Main.player[this.Index].velocity; NetMessage.SendData((int)PacketTypes.ItemDrop, -1, -1, NetworkText.FromLiteral(""), itemid, 0f, 0f, 0f); NetMessage.SendData((int)PacketTypes.ItemOwner, -1, -1, NetworkText.FromLiteral(""), itemid, 0f, 0f, 0f); } /// /// Sends an information message to the player. /// /// The message. public virtual void SendInfoMessage(string msg) { SendMessage(msg, Color.Yellow); } /// /// Sends an information message to the player. /// Replaces format items in the message with the string representation of a specified object. /// /// The message. /// An array of objects to format. public void SendInfoMessage(string format, params object[] args) { SendInfoMessage(string.Format(format, args)); } /// /// Sends a success message to the player. /// /// The message. public virtual void SendSuccessMessage(string msg) { SendMessage(msg, Color.Green); } /// /// Sends a success message to the player. /// Replaces format items in the message with the string representation of a specified object. /// /// The message. /// An array of objects to format. public void SendSuccessMessage(string format, params object[] args) { SendSuccessMessage(string.Format(format, args)); } /// /// Sends a warning message to the player. /// /// The message. public virtual void SendWarningMessage(string msg) { SendMessage(msg, Color.OrangeRed); } /// /// Sends a warning message to the player. /// Replaces format items in the message with the string representation of a specified object. /// /// The message. /// An array of objects to format. public void SendWarningMessage(string format, params object[] args) { SendWarningMessage(string.Format(format, args)); } /// /// Sends an error message to the player. /// /// The message. public virtual void SendErrorMessage(string msg) { SendMessage(msg, Color.Red); } /// /// Sends an error message to the player. /// Replaces format items in the message with the string representation of a specified object /// /// The message. /// An array of objects to format. public void SendErrorMessage(string format, params object[] args) { SendErrorMessage(string.Format(format, args)); } /// /// Sends a message with the specified color. /// /// The message. /// The message color. public virtual void SendMessage(string msg, Color color) { SendMessage(msg, color.R, color.G, color.B); } /// /// Sends a message with the specified RGB color. /// /// The message. /// The amount of red color to factor in. Max: 255. /// The amount of green color to factor in. Max: 255 /// The amount of blue color to factor in. Max: 255 public virtual void SendMessage(string msg, byte red, byte green, byte blue) { if (msg.Contains("\n")) { string[] msgs = msg.Split('\n'); foreach (string message in msgs) { SendMessage(message, red, green, blue); } return; } SendData(PacketTypes.SmartTextMessage, msg, 255, red, green, blue, -1); } /// /// Sends a message to the player with the specified RGB color. /// /// The message. /// The amount of red color to factor in. Max: 255. /// The amount of green color to factor in. Max: 255. /// The amount of blue color to factor in. Max: 255. /// The player who receives the message. public virtual void SendMessageFromPlayer(string msg, byte red, byte green, byte blue, int ply) { if (msg.Contains("\n")) { string[] msgs = msg.Split('\n'); foreach (string message in msgs) { SendMessageFromPlayer(message, red, green, blue, ply); } return; } SendDataFromPlayer(PacketTypes.SmartTextMessage, ply, msg, red, green, blue, -1); } /// /// Wounds the player with the given damage. /// /// The amount of damage the player will take. public virtual void DamagePlayer(int damage) { NetMessage.SendPlayerHurt(Index, PlayerDeathReason.LegacyDefault(), damage, (new Random()).Next(-1, 1), false, false, 0, -1, -1); } /// /// Kills the player. /// public virtual void KillPlayer() { NetMessage.SendPlayerDeath(Index, PlayerDeathReason.LegacyDefault(), 99999, (new Random()).Next(-1, 1), false, -1, -1); } /// /// Sets the player's team. /// /// The team color index. public virtual void SetTeam(int team) { Main.player[Index].team = team; NetMessage.SendData((int)PacketTypes.PlayerTeam, -1, -1, NetworkText.FromLiteral(""), Index); NetMessage.SendData((int)PacketTypes.PlayerTeam, -1, Index, NetworkText.FromLiteral(""), Index); } private DateTime LastDisableNotification = DateTime.UtcNow; /// /// Represents the ID of the chest that the player is viewing. /// public int ActiveChest = -1; /// /// Represents the current item the player is holding. /// public Item ItemInHand = new Item(); /// /// Disables the player for the given /// /// The reason why the player was disabled. /// Flags to dictate where this event is logged to. public virtual void Disable(string reason = "", DisableFlags flags = DisableFlags.WriteToLog) { LastThreat = DateTime.UtcNow; SetBuff(BuffID.Frozen, 330, true); SetBuff(BuffID.Stoned, 330, true); SetBuff(BuffID.Webbed, 330, true); if (ActiveChest != -1) { ActiveChest = -1; SendData(PacketTypes.ChestOpen, "", -1); } if (!string.IsNullOrEmpty(reason)) { if ((DateTime.UtcNow - LastDisableNotification).TotalMilliseconds > 5000) { if (flags.HasFlag(DisableFlags.WriteToConsole)) { if (flags.HasFlag(DisableFlags.WriteToLog)) { TShock.Log.ConsoleInfo("Player {0} has been disabled for {1}.", Name, reason); } else { Server.SendInfoMessage("Player {0} has been disabled for {1}.", Name, reason); } } LastDisableNotification = DateTime.UtcNow; } } /* * Calling new StackTrace() is incredibly expensive, and must be disabled * in release builds. Use a conditional call instead. */ LogStackFrame(); } [Conditional("DEBUG")] private void LogStackFrame() { var trace = new StackTrace(); StackFrame frame = null; frame = trace.GetFrame(1); if (frame != null && frame.GetMethod().DeclaringType != null) TShock.Log.Debug(frame.GetMethod().DeclaringType.Name + " called Disable()."); } /// /// Annoys the player for a specified amount of time. /// /// The public virtual void Whoopie(object time) { var time2 = (int) time; var launch = DateTime.UtcNow; var startname = Name; SendInfoMessage("You are now being annoyed."); while ((DateTime.UtcNow - launch).TotalSeconds < time2 && startname == Name) { SendData(PacketTypes.NpcSpecial, number: Index, number2: 2f); Thread.Sleep(50); } } /// /// Applies a buff to the player. /// /// The buff type. /// The buff duration. /// public virtual void SetBuff(int type, int time = 3600, bool bypass = false) { if ((DateTime.UtcNow - LastThreat).TotalMilliseconds < 5000 && !bypass) return; SendData(PacketTypes.PlayerAddBuff, number: Index, number2: type, number3: time); } //Todo: Separate this into a few functions. SendTo, SendToAll, etc /// /// Sends data to the player. /// /// The sent packet /// The packet text. /// /// /// /// /// public virtual void SendData(PacketTypes msgType, string text = "", int number = 0, float number2 = 0f, float number3 = 0f, float number4 = 0f, int number5 = 0) { if (RealPlayer && !ConnectionAlive) return; NetMessage.SendData((int) msgType, Index, -1, NetworkText.FromFormattable(text), number, number2, number3, number4, number5); } /// /// Sends data from the given player. /// /// The sent packet. /// The packet sender. /// The packet text. /// /// /// /// public virtual void SendDataFromPlayer(PacketTypes msgType, int ply, string text = "", float number2 = 0f, float number3 = 0f, float number4 = 0f, int number5 = 0) { if (RealPlayer && !ConnectionAlive) return; NetMessage.SendData((int) msgType, Index, -1, NetworkText.FromFormattable(text), ply, number2, number3, number4, number5); } /// /// Sends raw data to the player's socket object. /// /// The data to send. public virtual void SendRawData(byte[] data) { if (!RealPlayer || !ConnectionAlive) return; Netplay.Clients[Index].Socket.AsyncSend(data, 0, data.Length, Netplay.Clients[Index].ServerWriteCallBack); } /// /// Adds a command callback to a specified command string. /// /// The string representing the command i.e "yes" == /yes /// The method that will be executed on confirmation ie user accepts public void AddResponse( string name, Action callback) { if( AwaitingResponse.ContainsKey(name)) { AwaitingResponse.Remove(name); } AwaitingResponse.Add(name, callback); } /// /// Checks to see if a player has a specific permission. /// Fires the hook which may be handled to override permission checks. /// If the OnPlayerPermission hook is not handled and the player is assigned a temporary group, this method calls on the temporary group and returns the result. /// If the OnPlayerPermission hook is not handled and the player is not assigned a temporary group, this method calls on the player's current group. /// /// The permission to check. /// True if the player has that permission. public bool HasPermission(string permission) { if (PlayerHooks.OnPlayerPermission(this, permission)) return true; if (tempGroup != null) return tempGroup.HasPermission(permission); else return Group.HasPermission(permission); } } public class TSRestPlayer : TSPlayer { internal List CommandOutput = new List(); public TSRestPlayer(string playerName, Group playerGroup): base(playerName) { Group = playerGroup; AwaitingResponse = new Dictionary>(); } public override void SendMessage(string msg, Color color) { SendMessage(msg, color.R, color.G, color.B); } public override void SendMessage(string msg, byte red, byte green, byte blue) { this.CommandOutput.Add(msg); } public override void SendInfoMessage(string msg) { SendMessage(msg, Color.Yellow); } public override void SendSuccessMessage(string msg) { SendMessage(msg, Color.Green); } public override void SendWarningMessage(string msg) { SendMessage(msg, Color.OrangeRed); } public override void SendErrorMessage(string msg) { SendMessage(msg, Color.Red); } public List GetCommandOutput() { return this.CommandOutput; } } }