/* TShock, a server mod for Terraria Copyright (C) 2011-2019 Pryaxis & TShock Contributors 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; using System.Linq; 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"); /// /// Finds a TSPlayer based on name or ID /// /// Player name or ID /// A list of matching players public static List FindByNameOrID(string plr) { var found = new List(); // Avoid errors caused by null search if (plr == null) return found; byte plrID; if (byte.TryParse(plr, out plrID) && plrID < Main.maxPlayers) { TSPlayer player = TShock.Players[plrID]; if (player != null && player.Active) { return new List { player }; } } string plrLower = plr.ToLower(); foreach (TSPlayer player in TShock.Players) { if (player != null) { // Must be an EXACT match if (player.Name == plr) return new List { player }; if (player.Name.ToLower().StartsWith(plrLower)) found.Add(player); } } return found; } /// /// Used in preventing players from seeing the npc spawnrate permission error on join. /// internal bool HasReceivedNPCPermissionError { get; set; } /// /// 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; } /// /// 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; /// /// UserAccount object associated with the player. /// Set when the player logs in. /// public UserAccount Account { 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 { get => _respawnTimer; set => TPlayer.respawnTimer = (_respawnTimer = value) * 60; } private 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; /// Determines if the player is disabled by the SSC subsystem for not being logged in. public bool IsDisabledForSSC = false; /// Determines if the player is disabled by Bouncer for having hacked item stacks. public bool IsDisabledForStackDetection = false; /// Determines if the player is disabled by the item bans system for having banned wearables on the server. public bool IsDisabledForBannedWearable = false; /// Determines if the player is disabled for not clearing their trash. A re-login is the only way to reset this. public bool IsDisabledPendingTrashRemoval; /// Checks to see if active throttling is happening on events by Bouncer. Rejects repeated events by malicious clients in a short window. /// If the player is currently being throttled by Bouncer, or not. public bool IsBouncerThrottled() { return (DateTime.UtcNow - LastThreat).TotalMilliseconds < 5000; } /// Easy check if a player has any of IsDisabledForSSC, IsDisabledForStackDetection, IsDisabledForBannedWearable, or IsDisabledPendingTrashRemoval set. Or if they're not logged in and a login is required. /// If any of the checks that warrant disabling are set on this player. If true, Disable() is repeatedly called on them. public bool IsBeingDisabled() { return IsDisabledForSSC || IsDisabledForStackDetection || IsDisabledForBannedWearable || IsDisabledPendingTrashRemoval || !IsLoggedIn && TShock.Config.Settings.RequireLogin; } /// Checks to see if a player has hacked item stacks in their inventory, and messages them as it checks. /// If the check should send a message to the player with the results of the check. /// True if any stacks don't conform. public bool HasHackedItemStacks(bool shouldWarnPlayer = false) { // Iterates through each inventory location a player has. // This section is sub divided into number ranges for what each range of slots corresponds to. bool check = false; Item[] inventory = TPlayer.inventory; Item[] armor = TPlayer.armor; Item[] dye = TPlayer.dye; Item[] miscEquips = TPlayer.miscEquips; Item[] miscDyes = TPlayer.miscDyes; Item[] piggy = TPlayer.bank.item; Item[] safe = TPlayer.bank2.item; Item[] forge = TPlayer.bank3.item; Item[] voidVault = TPlayer.bank4.item; Item trash = TPlayer.trashItem; for (int i = 0; i < NetItem.MaxInventory; i++) { if (i < NetItem.InventoryIndex.Item2) { // From above: this is slots 0-58 in the inventory. // 0-58 Item item = new Item(); if (inventory[i] != null && inventory[i].netID != 0) { item.netDefaults(inventory[i].netID); item.Prefix(inventory[i].prefix); item.AffixName(); if (inventory[i].stack > item.maxStack || inventory[i].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove item {0} ({1}) and then rejoin.", item.Name, inventory[i].stack); } } } } else if (i < NetItem.ArmorIndex.Item2) { // 59-78 var index = i - NetItem.ArmorIndex.Item1; Item item = new Item(); if (armor[index] != null && armor[index].netID != 0) { item.netDefaults(armor[index].netID); item.Prefix(armor[index].prefix); item.AffixName(); if (armor[index].stack > item.maxStack || armor[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove armor {0} ({1}) and then rejoin.", item.Name, armor[index].stack); } } } } else if (i < NetItem.DyeIndex.Item2) { // 79-88 var index = i - NetItem.DyeIndex.Item1; Item item = new Item(); if (dye[index] != null && dye[index].netID != 0) { item.netDefaults(dye[index].netID); item.Prefix(dye[index].prefix); item.AffixName(); if (dye[index].stack > item.maxStack || dye[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove dye {0} ({1}) and then rejoin.", item.Name, dye[index].stack); } } } } else if (i < NetItem.MiscEquipIndex.Item2) { // 89-93 var index = i - NetItem.MiscEquipIndex.Item1; Item item = new Item(); if (miscEquips[index] != null && miscEquips[index].netID != 0) { item.netDefaults(miscEquips[index].netID); item.Prefix(miscEquips[index].prefix); item.AffixName(); if (miscEquips[index].stack > item.maxStack || miscEquips[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove item {0} ({1}) and then rejoin.", item.Name, miscEquips[index].stack); } } } } else if (i < NetItem.MiscDyeIndex.Item2) { // 93-98 var index = i - NetItem.MiscDyeIndex.Item1; Item item = new Item(); if (miscDyes[index] != null && miscDyes[index].netID != 0) { item.netDefaults(miscDyes[index].netID); item.Prefix(miscDyes[index].prefix); item.AffixName(); if (miscDyes[index].stack > item.maxStack || miscDyes[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove item dye {0} ({1}) and then rejoin.", item.Name, miscDyes[index].stack); } } } } else if (i < NetItem.PiggyIndex.Item2) { // 98-138 var index = i - NetItem.PiggyIndex.Item1; Item item = new Item(); if (piggy[index] != null && piggy[index].netID != 0) { item.netDefaults(piggy[index].netID); item.Prefix(piggy[index].prefix); item.AffixName(); if (piggy[index].stack > item.maxStack || piggy[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove piggy-bank item {0} ({1}) and then rejoin.", item.Name, piggy[index].stack); } } } } else if (i < NetItem.SafeIndex.Item2) { // 138-178 var index = i - NetItem.SafeIndex.Item1; Item item = new Item(); if (safe[index] != null && safe[index].netID != 0) { item.netDefaults(safe[index].netID); item.Prefix(safe[index].prefix); item.AffixName(); if (safe[index].stack > item.maxStack || safe[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove safe item {0} ({1}) and then rejoin.", item.Name, safe[index].stack); } } } } else if (i < NetItem.TrashIndex.Item2) { // 178-179 Item item = new Item(); if (trash != null && trash.netID != 0) { item.netDefaults(trash.netID); item.Prefix(trash.prefix); item.AffixName(); if (trash.stack > item.maxStack) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove trash item {0} ({1}) and then rejoin.", item.Name, trash.stack); } } } } else if (i < NetItem.ForgeIndex.Item2) { // 179-220 var index = i - NetItem.ForgeIndex.Item1; Item item = new Item(); if (forge[index] != null && forge[index].netID != 0) { item.netDefaults(forge[index].netID); item.Prefix(forge[index].prefix); item.AffixName(); if (forge[index].stack > item.maxStack || forge[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove Defender's Forge item {0} ({1}) and then rejoin.", item.Name, forge[index].stack); } } } } else if (i < NetItem.VoidIndex.Item2) { // 220-260 var index = i - NetItem.VoidIndex.Item1; Item item = new Item(); if (voidVault[index] != null && voidVault[index].netID != 0) { item.netDefaults(voidVault[index].netID); item.Prefix(voidVault[index].prefix); item.AffixName(); if (voidVault[index].stack > item.maxStack || voidVault[index].stack < 0) { check = true; if (shouldWarnPlayer) { SendErrorMessage("Stack cheat detected. Remove Void Vault item {0} ({1}) and then rejoin.", item.Name, voidVault[index].stack); } } } } } return check; } /// /// 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; /// /// Whether the player is accepting whispers from other users /// public bool AcceptingWhispers = true; /// Checks if a player is in range of a given tile if range checks are enabled. /// The x coordinate of the tile. /// The y coordinate of the tile. /// The range to check for. /// True if the player is in range of a tile or if range checks are off. False if not. public bool IsInRange(int x, int y, int range = 32) { int rgX = Math.Abs(TileX - x); int rgY = Math.Abs(TileY - y); if (TShock.Config.Settings.RangeChecks && ((rgX > range) || (rgY > range))) { TShock.Log.ConsoleDebug("Rangecheck failed for {0} ({1}, {2}) (rg: {3}/{5}, {4}/{5})", Name, x, y, rgX, rgY, range); return false; } return true; } private enum BuildPermissionFailPoint { GeneralBuild, SpawnProtect, Regions } /// Determines if the player can build on a given point. /// The x coordinate they want to build at. /// The y coordinate they want to build at. /// Whether or not the player should be warned if their build attempt fails /// True if the player can build at the given point from build, spawn, and region protection. public bool HasBuildPermission(int x, int y, bool shouldWarnPlayer = true) { BuildPermissionFailPoint failure = BuildPermissionFailPoint.GeneralBuild; // The goal is to short circuit on easy stuff as much as possible. // Don't compute permissions unless needed, and don't compute taxing stuff unless needed. // If the player has bypass on build protection or building is enabled; continue // (General build protection takes precedence over spawn protection) if (!TShock.Config.Settings.DisableBuild || HasPermission(Permissions.antibuild)) { failure = BuildPermissionFailPoint.SpawnProtect; // If they have spawn protect bypass, or it isn't spawn, or it isn't in spawn; continue // (If they have spawn protect bypass, we don't care if it's spawn or not) if (!TShock.Config.Settings.SpawnProtection || HasPermission(Permissions.editspawn) || !Utils.IsInSpawn(x, y)) { failure = BuildPermissionFailPoint.Regions; // If they have build permission in this region, then they're allowed to continue if (TShock.Regions.CanBuild(x, y, this)) { return true; } } } // If they lack build permission, they end up here. // If they have build permission but lack the ability to edit spawn and it's spawn, they end up here. // If they have build, it isn't spawn, or they can edit spawn, but they fail the region check, they end up here. // If they shouldn't be warned, exit early. if (!shouldWarnPlayer) return false; // Space out warnings by 2 seconds so that they don't get spammed. if (((DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond) - lastPermissionWarning) < 2000) { return false; } // If they should be warned, warn them. switch (failure) { case BuildPermissionFailPoint.GeneralBuild: SendErrorMessage("You do not have permission to build on this server."); break; case BuildPermissionFailPoint.SpawnProtect: SendErrorMessage("You do not have permission to build in the spawn point."); break; case BuildPermissionFailPoint.Regions: SendErrorMessage("You do not have permission to build in this region."); break; } // Set the last warning time to now. lastPermissionWarning = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; return false; } /// /// Determines if the player can build a multi-block tile object on a given point. /// Tile objects include things like Doors, Trap Doors, Item Frames, Beds, and Dressers. /// /// The x coordinate they want to build at. /// The y coordinate they want to build at. /// The width of the tile object /// The height of the tile object /// Whether or not the player should be warned if their build attempt fails /// True if the player can build at the given point from build, spawn, and region protection. public bool HasBuildPermissionForTileObject(int x, int y, int width, int height, bool shouldWarnPlayer = true) { for (int realx = x; realx < x + width; realx++) { for (int realy = y; realy < y + height; realy++) { if (!HasBuildPermission(realx, realy, shouldWarnPlayer)) { return false; } } } return true; } /// Determines if the player can paint on a given point. Checks general build permissions, then paint. /// The x coordinate they want to paint at. /// The y coordinate they want to paint at. /// True if they can paint. public bool HasPaintPermission(int x, int y) { return HasBuildPermission(x, y) && HasPermission(Permissions.canpaint); } /// Checks if a player can place ice, and if they can, tracks ice placements and removals. /// The x coordinate of the suspected ice block. /// The y coordinate of the suspected ice block. /// The tile type of the suspected ice block. /// The EditAction on the suspected ice block. /// True if a player successfully places an ice tile or removes one of their past ice tiles. public bool HasModifiedIceSuccessfully(int x, int y, short tileType, GetDataHandlers.EditAction editAction) { // The goal is to short circuit ASAP. // A subsequent call to HasBuildPermission can figure this out if not explicitly ice. if (!TShock.Config.Settings.AllowIce) { return false; } // They've placed some ice. Horrible! if (editAction == GetDataHandlers.EditAction.PlaceTile && tileType == TileID.MagicalIceBlock) { IceTiles.Add(new Point(x, y)); return true; } // The edit wasn't an add, so we check to see if the position matches any of the known ice tiles if (editAction == GetDataHandlers.EditAction.KillTile) { foreach (Point p in IceTiles) { // If they're trying to kill ice or dirt, and the tile was in the list, we allow it. if (p.X == x && p.Y == y && (Main.tile[p.X, p.Y].type == TileID.Dirt || Main.tile[p.X, p.Y].type == TileID.MagicalIceBlock)) { IceTiles.Remove(p); return true; } } } // Only a small number of cases let this happen. return false; } /// /// A list of points where ice tiles have been placed. /// public List IceTiles; /// /// The last time the player was warned for build permissions. /// In MS, defaults to 1 (so it will warn on the first attempt). /// public long lastPermissionWarning = 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; /// /// Keeps track of recently created projectiles by this player. TShock.cs OnSecondUpdate() removes from this in an async task. /// Projectiles older than 5 seconds are purged from this collection as they are no longer "recent." /// public List RecentlyCreatedProjectiles = new List(); /// /// 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 < 10; 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 " + Account.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); } } /// /// 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) { IsDisabledForSSC = true; if (!IsDisabledPendingTrashRemoval && (!Dead || TPlayer.difficulty != 2)) { PlayerData.CopyCharacter(this); TShock.CharacterDB.InsertPlayerData(this); } } PlayerData = new PlayerData(this); Group = TShock.Groups.GetGroupByName(TShock.Config.Settings.DefaultGuestGroupName); tempGroup = null; if (tempGroupTimer != null) { tempGroupTimer.Stop(); } Account = 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.Empty, 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.Empty, this.TPlayer.whoAmI, health); } /// /// Spawns the player at his spawn point. /// public void Spawn(PlayerSpawnContext context, int? respawnTimer = null) { if (this.sX > 0 && this.sY > 0) { Spawn(this.sX, this.sY, context, respawnTimer); } else { Spawn(TPlayer.SpawnX, TPlayer.SpawnY, context, respawnTimer); } } /// /// Spawns the player at the given coordinates. /// /// The X coordinate. /// The Y coordinate. /// The PlayerSpawnContext. /// The respawn timer, will be Player.respawnTimer if parameter is null. public void Spawn(int tilex, int tiley, PlayerSpawnContext context, int? respawnTimer = null) { using (var ms = new MemoryStream()) { var msg = new SpawnMsg { PlayerIndex = (byte)Index, TileX = (short)tilex, TileY = (short)tiley, RespawnTimer = respawnTimer ?? TShock.Players[Index].RespawnTimer * 60, PlayerSpawnContext = context, }; 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()); } } /// Sends a tile square at a location with a given size. /// Typically used to revert changes by Bouncer through sending the /// "old" version of modified data back to a client. /// Prevents desync issues. /// /// The x coordinate to send. /// The y coordinate to send. /// The size square set of tiles to send. /// true if the tile square was sent successfully, else false public virtual bool SendTileSquare(int x, int y, int size = 10) { return SendTileRect((short)x, (short)y, (byte)size, (byte)size); } /// /// Sends a rectangle of tiles at a location with the given length and width. /// /// The x coordinate the rectangle will begin at /// The y coordinate the rectangle will begin at /// The width of the rectangle /// The length of the rectangle /// Optional change type. Default None /// public virtual bool SendTileRect(short x, short y, byte width = 10, byte length = 10, TileChangeType changeType = TileChangeType.None) { try { NetMessage.SendTileSquare(Index, x, y, width, length, changeType); return true; } 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. /// /// The item ID. /// The item name. /// The item stack. /// The item prefix. /// True or false, depending if the item passed the check or not. public bool GiveItemCheck(int type, string name, int stack, int prefix = 0) { if ((TShock.ItemBans.DataModel.ItemIsBanned(name) && TShock.Config.Settings.PreventBannedItemSpawn) && (TShock.ItemBans.DataModel.ItemIsBanned(name, this) || !TShock.Config.Settings.AllowAllowedGroupsToSpawnBannedItems)) return false; GiveItem(type, stack, prefix); return true; } /// /// Gives an item to the player. /// /// The item ID. /// The item stack. /// The item prefix. public virtual void GiveItem(int type, int stack, int prefix = 0) { int itemIndex = Item.NewItem((int)X, (int)Y, TPlayer.width, TPlayer.height, type, stack, true, prefix, true); SendData(PacketTypes.ItemDrop, "", itemIndex); } /// /// 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; } if (this.Index == -1) //-1 is our broadcast index - this implies we're using TSPlayer.All.SendMessage and broadcasting to all clients { Terraria.Chat.ChatHelper.BroadcastChatMessage(NetworkText.FromLiteral(msg), new Color(red, green, blue)); } else { Terraria.Chat.ChatHelper.SendChatMessageToClient(NetworkText.FromLiteral(msg), new Color(red, green, blue), this.Index); } } /// /// 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; } Terraria.Chat.ChatHelper.BroadcastChatMessageAs((byte)ply, NetworkText.FromLiteral(msg), new Color(red, green, blue)); } /// /// Sends the text of a given file to the player. Replacement of %map% and %players% if in the file. /// /// Filename relative to public void SendFileTextAsMessage(string file) { string foo = ""; bool containsOldFormat = false; using (var tr = new StreamReader(file)) { Color lineColor; while ((foo = tr.ReadLine()) != null) { lineColor = Color.White; if (string.IsNullOrWhiteSpace(foo)) { continue; } var players = new List(); foreach (TSPlayer ply in TShock.Players) { if (ply != null && ply.Active) { players.Add(ply.Name); } } foo = foo.Replace("%map%", (TShock.Config.Settings.UseServerName ? TShock.Config.Settings.ServerName : Main.worldName)); foo = foo.Replace("%players%", String.Join(",", players)); SendMessage(foo, lineColor); } } } /// /// 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.Empty, 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.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(); } /// /// Disconnects this player from the server with a reason. /// /// The reason to display to the user and to the server on kick. /// If the kick should happen regardless of immunity to kick permissions. /// If no message should be broadcasted to the server. /// The originator of the kick, for display purposes. /// If the player's server side character should be saved on kick. public bool Kick(string reason, bool force = false, bool silent = false, string adminUserName = null, bool saveSSI = false) { if (!ConnectionAlive) return true; if (force || !HasPermission(Permissions.immunetokick)) { SilentKickInProgress = silent; if (IsLoggedIn && saveSSI) SaveServerCharacter(); Disconnect(string.Format("Kicked: {0}", reason)); TShock.Log.ConsoleInfo(string.Format("Kicked {0} for : '{1}'", Name, reason)); string verb = force ? "force " : ""; if (!silent) { if (string.IsNullOrWhiteSpace(adminUserName)) TShock.Utils.Broadcast(string.Format("{0} was {1}kicked for '{2}'", Name, verb, reason.ToLower()), Color.Green); else TShock.Utils.Broadcast(string.Format("{0} {1}kicked {2} for '{3}'", adminUserName, verb, Name, reason.ToLower()), Color.Green); } return true; } return false; } /// /// Bans and disconnects the player from the server. /// /// The reason to be displayed to the server. /// If the ban should bypass immunity to ban checks. /// The player who initiated the ban. public bool Ban(string reason, bool force = false, string adminUserName = null) { if (!ConnectionAlive) return true; if (force) { TShock.Bans.InsertBan($"{Identifier.IP}{IP}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); TShock.Bans.InsertBan($"{Identifier.UUID}{UUID}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); if (Account != null) { TShock.Bans.InsertBan($"{Identifier.Account}{Account.Name}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); } Disconnect(string.Format("Banned: {0}", reason)); string verb = force ? "force " : ""; if (string.IsNullOrWhiteSpace(adminUserName)) TSPlayer.All.SendInfoMessage("{0} was {1}banned for '{2}'.", Name, verb, reason); else TSPlayer.All.SendInfoMessage("{0} {1}banned {2} for '{3}'.", adminUserName, verb, Name, reason); return true; } return false; } /// /// Sends the player an error message stating that more than one match was found /// appending a csv list of the matches. /// /// An enumerable list with the matches public void SendMultipleMatchError(IEnumerable matches) { SendErrorMessage("More than one match found: "); var lines = PaginationTools.BuildLinesFromTerms(matches.ToArray()); lines.ForEach(SendInfoMessage); SendErrorMessage("Use \"my query\" for items with spaces."); } [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, text == null ? null : NetworkText.FromLiteral(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) { PermissionHookResult hookResult = PlayerHooks.OnPlayerPermission(this, permission); if (hookResult != PermissionHookResult.Unhandled) return hookResult == PermissionHookResult.Granted; if (tempGroup != null) return tempGroup.HasPermission(permission); else return Group.HasPermission(permission); } /// /// Checks to see if a player has permission to use the specific banned item. /// Fires the hook which may be handled to override item ban permission checks. /// /// The to check. /// True if the player has permission to use the banned item. public bool HasPermission(ItemBan bannedItem) { return TShock.ItemBans.DataModel.ItemIsBanned(bannedItem.Name, this); } /// /// Checks to see if a player has permission to use the specific banned projectile. /// Fires the hook which may be handled to override projectile ban permission checks. /// /// The to check. /// True if the player has permission to use the banned projectile. public bool HasPermission(ProjectileBan bannedProj) { return TShock.ProjectileBans.ProjectileIsBanned(bannedProj.ID, this); } /// /// Checks to see if a player has permission to use the specific banned tile. /// Fires the hook which may be handled to override tile ban permission checks. /// /// The to check. /// True if the player has permission to use the banned tile. public bool HasPermission(TileBan bannedTile) { return TShock.TileBans.TileIsBanned(bannedTile.ID, this); } } 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; } } }