/*
TShock, a server mod for Terraria
Copyright (C) 2011-2022 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 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;
using Terraria.GameContent.Creative;
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
}
///
/// An enum based on the current client's connection state to the server.
///
public enum ConnectionState : int
{
///
/// The server is password protected and the connection is pending until a password is sent by the client.
///
AwaitingPassword = -1,
///
/// The connection has been established, and the client must verify its version.
///
AwaitingVersionCheck = 0,
///
/// The server has accepted the client's password to connect and/or the server has verified the client's version string as being correct. The client is now being assigned a player slot.
///
AssigningPlayerSlot = 1,
///
/// The player slot has been received by the client, and the server is now waiting for the player information.
///
AwaitingPlayerInfo = 2,
///
/// Player information has been received, and the client is requesting world data.
///
RequestingWorldData = 3,
///
/// The world data is being sent to the client.
///
ReceivingWorldData = 4,
///
/// The world data has been received, and the client is now finalizing the load.
///
FinalizingWorldLoad = 5,
///
/// The client is requesting tile data.
///
RequestingTileData = 6,
///
/// The connection process is complete (The player has spawned), and the client has fully joined the game.
///
Complete = 10
}
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.
/// If the string comes with tsi: or tsn:, we'll only return a list with one element,
/// either the player with the matching ID or name, respectively.
///
/// Player name or ID
/// A list of matching players
public static List FindByNameOrID(string search)
{
var found = new List();
search = search.Trim();
// tsi: and tsn: are used to disambiguate between usernames and not
// and are also both 3 characters to remove them from the search
// (the goal was to pick prefixes unlikely to be used by names)
// (and not to collide with other prefixes used by other commands)
var exactIndexOnly = search.StartsWith("tsi:");
var exactNameOnly = search.StartsWith("tsn:");
if (exactNameOnly || exactIndexOnly)
search = search.Remove(0, 4);
// Avoid errors caused by null search
if (search == null || search == "")
return found;
byte searchID;
if (byte.TryParse(search, out searchID) && searchID < Main.maxPlayers)
{
TSPlayer player = TShock.Players[searchID];
if (player != null && player.Active)
{
if (exactIndexOnly)
return new List { player };
found.Add(player);
}
}
string searchLower = search.ToLower();
foreach (TSPlayer player in TShock.Players)
{
if (player != null)
{
if ((search == player.Name) && exactNameOnly)
return new List { player };
if (player.Name.ToLower().StartsWith(searchLower))
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 bool initialSpawn = false;
public int initialServerSpawnX = -2;
public int initialServerSpawnY = -2;
public bool spawnSynced = false;
public int initialClientSpawnX = -2;
public int initialClientSpawnY = -2;
///
/// 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;
/// Determines if the player has finished the handshake (Sent all necessary packets for connection, such as Request World Data, Spawn Player, etc). A normal client would do all of this no problem.
public bool FinishedHandshake = false;
/// Server-side character's recorded death count.
public int sscDeathsPVE = 0;
/// Server-side character's recorded PVP death count.
public int sscDeathsPVP = 0;
///
/// Gets the player's total death count.
/// If server-side characters are enabled and player doesn't have bypass permission,
/// returns the server-stored value; otherwise returns the client's value.
///
public int DeathsPVE
{
get
{
if (Main.ServerSideCharacter && !this.HasPermission(Permissions.bypassssc))
{
return sscDeathsPVE;
}
return this.TPlayer.numberOfDeathsPVE;
}
}
///
/// Gets the player's total PVP death count.
/// If server-side characters are enabled and player doesn't have bypass permission,
/// returns the server-stored value; otherwise returns the client's value.
///
public int DeathsPVP
{
get
{
if (Main.ServerSideCharacter && !this.HasPermission(Permissions.bypassssc))
{
return sscDeathsPVP;
}
return this.TPlayer.numberOfDeathsPVP;
}
}
/// 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[] loadout1Armor = TPlayer.Loadouts[0].Armor;
Item[] loadout1Dye = TPlayer.Loadouts[0].Dye;
Item[] loadout2Armor = TPlayer.Loadouts[1].Armor;
Item[] loadout2Dye = TPlayer.Loadouts[1].Dye;
Item[] loadout3Armor = TPlayer.Loadouts[2].Armor;
Item[] loadout3Dye = TPlayer.Loadouts[2].Dye;
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].type != 0)
{
item.netDefaults(inventory[i].type);
item.Prefix(inventory[i].prefix);
item.AffixName();
if (inventory[i].stack > item.maxStack || inventory[i].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(armor[index].type);
item.Prefix(armor[index].prefix);
item.AffixName();
if (armor[index].stack > item.maxStack || armor[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(dye[index].type);
item.Prefix(dye[index].prefix);
item.AffixName();
if (dye[index].stack > item.maxStack || dye[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(miscEquips[index].type);
item.Prefix(miscEquips[index].prefix);
item.AffixName();
if (miscEquips[index].stack > item.maxStack || miscEquips[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(miscDyes[index].type);
item.Prefix(miscDyes[index].prefix);
item.AffixName();
if (miscDyes[index].stack > item.maxStack || miscDyes[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(piggy[index].type);
item.Prefix(piggy[index].prefix);
item.AffixName();
if (piggy[index].stack > item.maxStack || piggy[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(safe[index].type);
item.Prefix(safe[index].prefix);
item.AffixName();
if (safe[index].stack > item.maxStack || safe[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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.type != 0)
{
item.netDefaults(trash.type);
item.Prefix(trash.prefix);
item.AffixName();
if (trash.stack > item.maxStack)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(forge[index].type);
item.Prefix(forge[index].prefix);
item.AffixName();
if (forge[index].stack > item.maxStack || forge[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("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].type != 0)
{
item.netDefaults(voidVault[index].type);
item.Prefix(voidVault[index].prefix);
item.AffixName();
if (voidVault[index].stack > item.maxStack || voidVault[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Void Vault item {0} ({1}) and then rejoin.", item.Name, voidVault[index].stack));
}
}
}
}
else if (i < NetItem.Loadout1Armor.Item2)
{
var index = i - NetItem.Loadout1Armor.Item1;
Item item = new Item();
if (loadout1Armor[index] != null && loadout1Armor[index].type != 0)
{
item.netDefaults(loadout1Armor[index].type);
item.Prefix(loadout1Armor[index].prefix);
item.AffixName();
if (loadout1Armor[index].stack > item.maxStack || loadout1Armor[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Loadout 1 item {0} ({1}) and then rejoin.", item.Name, loadout1Armor[index].stack));
}
}
}
}
else if (i < NetItem.Loadout1Dye.Item2)
{
var index = i - NetItem.Loadout1Dye.Item1;
Item item = new Item();
if (loadout1Dye[index] != null && loadout1Dye[index].type != 0)
{
item.netDefaults(loadout1Dye[index].type);
item.Prefix(loadout1Dye[index].prefix);
item.AffixName();
if (loadout1Dye[index].stack > item.maxStack || loadout1Dye[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Loadout 1 item {0} ({1}) and then rejoin.", item.Name, loadout1Dye[index].stack));
}
}
}
}
else if (i < NetItem.Loadout2Armor.Item2)
{
var index = i - NetItem.Loadout2Armor.Item1;
Item item = new Item();
if (loadout2Armor[index] != null && loadout2Armor[index].type != 0)
{
item.netDefaults(loadout2Armor[index].type);
item.Prefix(loadout2Armor[index].prefix);
item.AffixName();
if (loadout2Armor[index].stack > item.maxStack || loadout2Armor[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Loadout 2 item {0} ({1}) and then rejoin.", item.Name, loadout2Armor[index].stack));
}
}
}
}
else if (i < NetItem.Loadout2Dye.Item2)
{
var index = i - NetItem.Loadout2Dye.Item1;
Item item = new Item();
if (loadout2Dye[index] != null && loadout2Dye[index].type != 0)
{
item.netDefaults(loadout2Dye[index].type);
item.Prefix(loadout2Dye[index].prefix);
item.AffixName();
if (loadout2Dye[index].stack > item.maxStack || loadout2Dye[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Loadout 2 item {0} ({1}) and then rejoin.", item.Name, loadout2Dye[index].stack));
}
}
}
}
else if (i < NetItem.Loadout3Armor.Item2)
{
var index = i - NetItem.Loadout3Armor.Item1;
Item item = new Item();
if (loadout3Armor[index] != null && loadout3Armor[index].type != 0)
{
item.netDefaults(loadout3Armor[index].type);
item.Prefix(loadout3Armor[index].prefix);
item.AffixName();
if (loadout3Armor[index].stack > item.maxStack || loadout3Armor[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Loadout 3 item {0} ({1}) and then rejoin.", item.Name, loadout3Armor[index].stack));
}
}
}
}
else if (i < NetItem.Loadout3Dye.Item2)
{
var index = i - NetItem.Loadout3Dye.Item1;
Item item = new Item();
if (loadout3Dye[index] != null && loadout3Dye[index].type != 0)
{
item.netDefaults(loadout3Dye[index].type);
item.Prefix(loadout3Dye[index].prefix);
item.AffixName();
if (loadout3Dye[index].stack > item.maxStack || loadout3Dye[index].stack < 0)
{
check = true;
if (shouldWarnPlayer)
{
SendErrorMessage(GetString("Stack cheat detected. Remove Loadout 3 item {0} ({1}) and then rejoin.", item.Name, loadout3Dye[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(GetString("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)
{
PermissionHookResult hookResult = PlayerHooks.OnPlayerHasBuildPermission(this, x, y);
if (hookResult != PermissionHookResult.Unhandled)
{
return hookResult == PermissionHookResult.Granted;
}
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.
if (!TShock.Config.Settings.SuppressPermissionFailureNotices)
{
switch (failure)
{
case BuildPermissionFailPoint.GeneralBuild:
SendErrorMessage(GetString("You do not have permission to build on this server."));
break;
case BuildPermissionFailPoint.SpawnProtect:
SendErrorMessage(GetString("You do not have permission to build in the spawn point."));
break;
case BuildPermissionFailPoint.Regions:
SendErrorMessage(GetString("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;
///
/// Controls the journey godmode
///
public bool GodMode
{
get =>
CreativePowerManager.Instance.GetPower().IsEnabledForPlayer(Index);
set =>
CreativePowerManager.Instance.GetPower().SetEnabledState(Index, value);
}
///
/// 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
&& (Client != null && Client.IsActive && !Client.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 Client.State; }
set { Client.State = value; }
}
///
/// Gets the player's UUID.
///
public string UUID
{
get { return RealPlayer ? Client.ClientUUID : ""; }
}
///
/// Gets the player's IP.
///
public string IP
{
get
{
if (string.IsNullOrEmpty(CacheIP))
return
CacheIP = RealPlayer ? (Client.Socket.IsConnected()
? TShock.Utils.GetRealIP(Client.Socket.GetRemoteAddress().ToString())
: "")
: "127.0.0.1";
else
return CacheIP;
}
}
///
/// Gets the player's inventory (first 5 rows)
///
public IEnumerable Inventory
{
get
{
for (int i = 0; i < 50; i++)
yield return TPlayer.inventory[i];
}
}
///
/// 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(GetString($"Skipping SSC save (due to tshock.ignore.ssc) for {Account.Name}"));
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;
}
}
///
/// Player RemoteClient.
///
public RemoteClient Client => Netplay.Clients[Index];
///
/// 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 PvP player mode.
///
public bool Hostile => TPlayer.hostile;
///
/// 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 coordinate 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();
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>();
if (playerName == "All" || playerName == "Server")
FinishedHandshake = true; //Hot fix for the all player object not getting packets like TimeSet, etc because they have no state and finished handshake will always be false.
}
///
/// 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(GetString("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;
}
SendTileSquareCentered((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;
}
///
/// Teleports the player to their spawnpoint.
/// Teleports to main spawnpoint if their bed is not active.
/// Supports SSC.
///
public bool TeleportSpawnpoint()
{
// NOTE: it is vanilla behaviour to not permanently override the spawnpoint if the bed spawn is broken/invalid
int x = TPlayer.SpawnX;
int y = TPlayer.SpawnY;
if ((x == -1 && y == -1) ||
!Main.tile[x, y - 1].active() || Main.tile[x, y - 1].type != TileID.Beds || !WorldGen.StartRoomCheck(x, y - 1))
{
x = Main.spawnTileX;
y = Main.spawnTileY;
}
return Teleport(x * 16, y * 16 - 48);
}
///
/// 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)
{
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.
/// The number of deaths PVE, will be TPlayer.numberOfDeathsPVE if parameter is null.
/// The number of deaths PVP, will be TPlayer.numberOfDeathsPVP if parameter is null.
public void Spawn(int tilex, int tiley, PlayerSpawnContext context, int? respawnTimer = null, short? numberOfDeathsPVE = null, short? numberOfDeathsPVP = 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,
NumberOfDeathsPVE = numberOfDeathsPVE ?? (short)TPlayer.numberOfDeathsPVE,
NumberOfDeathsPVP = numberOfDeathsPVP ?? (short)TPlayer.numberOfDeathsPVP,
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
[Obsolete("This method may not send tiles the way you would expect it to. The (x,y) coordinates are the top left corner of the tile square, switch to " + nameof(SendTileSquareCentered) + " if you wish for the coordindates to be the center of the square.")]
public virtual bool SendTileSquare(int x, int y, int size = 10)
{
return SendTileRect((short)x, (short)y, (byte)size, (byte)size);
}
///
/// Sends a tile square at a center 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 coordinates of the center of the square.
/// The y coordinates of the center of the square.
/// The size square set of tiles to send.
/// true if the tile square was sent successfully, else false
public virtual bool SendTileSquareCentered(int x, int y, byte size = 10)
{
return SendTileRect((short)(x - (size / 2)), (short)(y - (size / 2)), size, 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;
}
///
/// Changes the values of the array.
///
/// The area of the sections you want to set a value to.
/// The minimum size should be set to 200x150. If null, then the entire map is specified.
/// Is the section loaded.
// The server does not send the player the whole world, it sends it in sections. To do this, it sets up visible and invisible sections.
// If the player was not in any section(Client.TileSections[x, y] == false) then the server will send the missing section of the world.
// This method allows you to simulate what the player has or has not seen these sections.
// For example, we can put some number of earths blocks in some vast area, for example, for the whole world, but the player will not see the changes, because some section is already loaded for him. At this point this method can come into effect! With it we will be able to select some zone and make it both visible and invisible to the player.
// The server will assume that the zone is not loaded on the player, and will resend the data, but with earth blocks.
public void UpdateSection(Rectangle? rectangle = null, bool isLoaded = false)
{
if (rectangle.HasValue)
{
for (int i = Netplay.GetSectionX(rectangle.Value.X); i < Netplay.GetSectionX(rectangle.Value.X + rectangle.Value.Width) && i < Main.maxSectionsX; i++)
{
for (int j = Netplay.GetSectionY(rectangle.Value.Y); j < Netplay.GetSectionY(rectangle.Value.Y + rectangle.Value.Height) && j < Main.maxSectionsY; j++)
{
Client.TileSections[i, j] = isLoaded;
}
}
}
else
{
for (int i = 0; i < Main.maxSectionsX; i++)
{
for (int j = 0; j < Main.maxSectionsY; j++)
{
Client.TileSections[i, j] = isLoaded;
}
}
}
}
///
/// 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)
{
if (TShock.Config.Settings.GiveItemsDirectly)
GiveItemDirectly(type, stack, prefix);
else
GiveItemByDrop(type, stack, prefix);
}
///
/// Gives an item to the player.
///
/// Item with data to be given to the player.
public virtual void GiveItem(NetItem item)
{
GiveItem(item.NetId, item.Stack, item.PrefixId);
}
private Item EmptySentinelItem = new Item();
private bool Depleted(Item item)
=> item.type == 0 || item.stack == 0;
private void GiveItemDirectly(int type, int stack, int prefix)
{
if (ItemID.Sets.IsAPickup[type] || !Main.ServerSideCharacter || this.IsDisabledForSSC)
{
GiveItemByDrop(type, stack, prefix);
return;
}
var item = new Item();
item.netDefaults(type);
item.stack = stack;
item.prefix = (byte)prefix;
if (item.IsACoin)
for (int slot = -4; slot < 50; slot++)
if (Depleted(item = GiveItemDirectly_FillIntoOccupiedSlot(item, slot < 0 ? slot + 54 : slot)))
return;
if (item.FitsAmmoSlot())
if (Depleted(item = GiveItem_FillAmmo(item)))
return;
for (int slot = 0; slot < 50; slot++)
if (Depleted(item = GiveItemDirectly_FillIntoOccupiedSlot(item, slot)))
return;
if (!item.IsACoin && item.useStyle != 0)
for (int slot = 0; slot < 10; slot++)
if (Depleted(item = GiveItemDirectly_FillEmptyInventorySlot(item, slot)))
return;
int lastSlot = item.IsACoin ? 54 : 50;
for (int slot = lastSlot - 1; slot >= 0; slot--)
if (Depleted(item = GiveItemDirectly_FillEmptyInventorySlot(item, slot)))
return;
// oh no, i can't give the rest of the items... guess i gotta spill it on the floor
GiveItemByDrop(item.type, item.stack, item.prefix);
}
private void SendItemSlotPacketFor(int slot)
{
int prefix = this.TPlayer.inventory[slot].prefix;
NetMessage.SendData(5, this.Index, -1, null, this.Index, slot, prefix, 0f, 0, 0, 0);
}
private Item GiveItem_FillAmmo(Item item)
{
var inv = this.TPlayer.inventory;
for (int i = 54; i < 58; i++)
if (Depleted(item = GiveItemDirectly_FillIntoOccupiedSlot(item, i)))
return EmptySentinelItem;
if (!item.CanFillEmptyAmmoSlot())
return item;
for (int i = 54; i < 58; i++)
if (GiveItemDirectly_FillEmptyInventorySlot(item, i) == EmptySentinelItem)
return EmptySentinelItem;
return item;
}
private Item GiveItemDirectly_FillIntoOccupiedSlot(Item item, int slot)
{
var inv = this.TPlayer.inventory;
if (inv[slot].type <= 0 || inv[slot].stack >= inv[slot].maxStack || item.IsNotTheSameAs(inv[slot]))
return item;
if (item.stack + inv[slot].stack <= inv[slot].maxStack)
{
inv[slot].stack += item.stack;
SendItemSlotPacketFor(slot);
return EmptySentinelItem;
}
var newItem = item.DeepClone();
newItem.stack -= inv[slot].maxStack - inv[slot].stack;
inv[slot].stack = inv[slot].maxStack;
SendItemSlotPacketFor(slot);
return newItem;
}
private Item GiveItemDirectly_FillEmptyInventorySlot(Item item, int slot)
{
var inv = this.TPlayer.inventory;
if (inv[slot].type != 0)
return item;
inv[slot] = item;
SendItemSlotPacketFor(slot);
return EmptySentinelItem;
}
private void GiveItemByDrop(int type, int stack, int prefix)
{
int itemIndex = Item.NewItem(new EntitySource_DebugCommand(), (int)X, (int)Y, TPlayer.width, TPlayer.height, type, stack, true, prefix, true);
Main.item[itemIndex].playerIndexTheItemIsReservedFor = this.Index;
SendData(PacketTypes.ItemDrop, "", itemIndex, 1);
SendData(PacketTypes.ItemOwner, null, 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.LimeGreen);
}
///
/// 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));
foo = foo.Replace("%specifier%", TShock.Config.Settings.CommandSpecifier);
foo = foo.Replace("%onlineplayers%", TShock.Utils.GetActivePlayerCount().ToString());
foo = foo.Replace("%serverslots%", TShock.Config.Settings.MaxSlots.ToString());
SendMessage(foo, lineColor);
}
}
}
///
/// Wounds the player with the given damage.
///
/// The amount of damage the player will take.
public virtual void DamagePlayer(int damage)
{
DamagePlayer(damage, PlayerDeathReason.LegacyDefault());
}
///
/// Wounds the player with the given damage.
///
/// The amount of damage the player will take.
/// The reason for causing damage to player.
public virtual void DamagePlayer(int damage, PlayerDeathReason reason)
{
NetMessage.SendPlayerHurt(Index, reason, damage, (new Random()).Next(-1, 1), false, false, 0, -1, -1);
}
///
/// Kills the player.
///
public virtual void KillPlayer()
{
KillPlayer(PlayerDeathReason.LegacyDefault());
}
///
/// Kills the player.
///
/// Reason for killing a player.
public virtual void KillPlayer(PlayerDeathReason reason)
{
NetMessage.SendPlayerDeath(Index, reason, 99999, (new Random()).Next(-1, 1), false, -1, -1);
}
///
/// Sets the player's team.
///
/// The team color index.
public virtual void SetTeam(int team)
{
if (team < 0 || team >= Main.teamColor.Length)
throw new ArgumentException("The player's team is not in the range of available.");
Main.player[Index].team = team;
NetMessage.SendData((int)PacketTypes.PlayerTeam, -1, -1, NetworkText.Empty, Index);
}
///
/// Sets the player's pvp.
///
/// The state of the pvp mode.
/// Whether a chat message about the change should be sent.
public virtual void SetPvP(bool mode, bool withMsg = false)
{
Main.player[Index].hostile = mode;
NetMessage.SendData((int)PacketTypes.TogglePvp, -1, -1, NetworkText.Empty, Index);
if (withMsg)
TSPlayer.All.SendMessage(Language.GetTextValue(mode ? "LegacyMultiplayer.11" : "LegacyMultiplayer.12", Name), Main.teamColor[Team]);
}
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(GetString("Player {0} has been disabled for {1}.", Name, reason));
}
else
{
Server.SendInfoMessage(GetString("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(GetString("Kicked: {0}", reason));
TShock.Log.ConsoleInfo(GetString("Kicked {0} for : '{1}'", Name, reason));
if (!silent)
{
if (string.IsNullOrWhiteSpace(adminUserName))
TShock.Utils.Broadcast(GetString("{0} was kicked for '{1}'", Name, reason), Color.Green);
else
TShock.Utils.Broadcast(GetString("{0} kicked {1} for '{2}'", adminUserName, Name, reason), Color.Green);
}
return true;
}
return false;
}
///
/// Bans and disconnects the player from the server.
///
/// The reason to be displayed to the server.
/// The player who initiated the ban.
public bool Ban(string reason, string adminUserName = null)
{
if (!ConnectionAlive)
return true;
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(GetString("Banned: {0}", reason));
if (string.IsNullOrWhiteSpace(adminUserName))
TSPlayer.All.SendInfoMessage(GetString("{0} was banned for '{1}'.", Name, reason));
else
TSPlayer.All.SendInfoMessage(GetString("{0} banned {1} for '{2}'.", adminUserName, Name, reason));
return true;
}
///
/// 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