/*
TShock, a server mod for Terraria
Copyright (C) 2011-2015 Nyx Studios (fka. The TShock Team)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Terraria;
using Terraria.ID;
using TShockAPI.DB;
using BCrypt.Net;
namespace TShockAPI
{
///
/// Utilities and other TShock core calls that don't fit anywhere else
///
public class Utils
{
///
/// The lowest id for a prefix.
///
private const int FirstItemPrefix = 1;
///
/// The highest id for a prefix.
///
private const int LastItemPrefix = 83;
/// instance - an instance of the utils class
private static readonly Utils instance = new Utils();
/// Utils - Creates a utilities object.
private Utils() {}
/// Instance - An instance of the utils class.
/// value - the Utils instance
public static Utils Instance { get { return instance; } }
/// Random - An instance of random for generating random data.
[Obsolete("Please create your own random objects; this will be removed in the next version of TShock.")]
public Random Random = new Random();
///
/// Provides the real IP address from a RemoteEndPoint string that contains a port and an IP
///
/// A string IPv4 address in IP:PORT form.
/// A string IPv4 address.
public string GetRealIP(string mess)
{
return mess.Split(':')[0];
}
///
/// Returns a list of current players on the server
///
/// bool includeIDs - whether or not the string of each player name should include ID data
/// List of strings with names
public List GetPlayers(bool includeIDs)
{
var players = new List();
foreach (TSPlayer ply in TShock.Players)
{
if (ply != null && ply.Active)
{
if (includeIDs)
{
players.Add(String.Format("{0} (IX: {1}{2})", ply.Name, ply.Index, ply.User != null ? ", ID: " + ply.User.ID : ""));
}
else
{
players.Add(ply.Name);
}
}
}
return players;
}
///
/// Finds a player and gets IP as string
///
/// string playername
public string GetPlayerIP(string playername)
{
foreach (TSPlayer player in TShock.Players)
{
if (player != null && player.Active)
{
if (playername.ToLower() == player.Name.ToLower())
{
return player.IP;
}
}
}
return null;
}
///
/// It's a clamp function
///
///
/// Value to clamp
/// Maximum bounds of the clamp
/// Minimum bounds of the clamp
///
public T Clamp(T value, T max, T min)
where T : IComparable
{
T result = value;
if (value.CompareTo(max) > 0)
result = max;
if (value.CompareTo(min) < 0)
result = min;
return result;
}
///
/// Saves the map data by calling the SaveManager and instructing it to save the world.
///
public void SaveWorld()
{
SaveManager.Instance.SaveWorld();
}
/// Broadcast - Broadcasts a message to all players on the server, as well as the server console, and the logs.
/// msg - The message to send
/// red - The amount of red (0-255) in the color for supported destinations.
/// green - The amount of green (0-255) in the color for supported destinations.
/// blue - The amount of blue (0-255) in the color for the supported destinations.
public void Broadcast(string msg, byte red, byte green, byte blue)
{
TSPlayer.All.SendMessage(msg, red, green, blue);
TSPlayer.Server.SendMessage(msg, red, green, blue);
TShock.Log.Info(string.Format("Broadcast: {0}", msg));
}
/// >Broadcast - Broadcasts a message to all players on the server, as well as the server console, and the logs.
/// msg - The message to send
/// color - The color object for supported destinations.
public void Broadcast(string msg, Color color)
{
Broadcast(msg, color.R, color.G, color.B);
}
///
/// Broadcasts a message from a Terraria playerplayer, not TShock
///
/// ply - the Terraria player index that will send the packet
/// msg - The message to send
/// red - The amount of red (0-255) in the color for supported destinations.
/// green - The amount of green (0-255) in the color for supported destinations.
/// blue - The amount of blue (0-255) in the color for the supported destinations.
public void Broadcast(int ply, string msg, byte red, byte green, byte blue)
{
TSPlayer.All.SendMessageFromPlayer(msg, red, green, blue, ply);
TSPlayer.Server.SendMessage(Main.player[ply].name + ": " + msg, red, green, blue);
TShock.Log.Info(string.Format("Broadcast: {0}", Main.player[ply].name + ": " + msg));
}
///
/// Sends message to all players with 'logs' permission.
///
/// Message to send
/// Color of the message
/// The player to not send the message to.
public void SendLogs(string log, Color color, TSPlayer excludedPlayer = null)
{
TShock.Log.Info(log);
TSPlayer.Server.SendMessage(log, color);
foreach (TSPlayer player in TShock.Players)
{
if (player != null && player != excludedPlayer && player.Active && player.HasPermission(Permissions.logs) &&
player.DisplayLogs && TShock.Config.DisableSpewLogs == false)
player.SendMessage(log, color);
}
}
///
/// Gets the number of active players on the server.
///
/// The number of active players on the server.
public int ActivePlayers()
{
return Main.player.Where(p => null != p && p.active).Count();
}
///
/// Finds a TSPlayer based on name or ID
///
/// Player name or ID
/// A list of matching players
public List FindPlayer(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;
}
//Random should not be generated in a method
Random r = new Random();
///
/// Gets a random clear tile in range
///
/// Bound X
/// Bound Y
/// Range on the X axis
/// Range on the Y axis
/// X location
/// Y location
public void GetRandomClearTileWithInRange(int startTileX, int startTileY, int tileXRange, int tileYRange,
out int tileX, out int tileY)
{
int j = 0;
do
{
if (j == 100)
{
tileX = startTileX;
tileY = startTileY;
break;
}
tileX = startTileX + r.Next(tileXRange*-1, tileXRange);
tileY = startTileY + r.Next(tileYRange*-1, tileYRange);
j++;
} while (TilePlacementValid(tileX, tileY) && TileSolid(tileX, tileY));
}
///
/// Determines if a tile is valid.
///
/// Location X
/// Location Y
/// If the tile is valid
public bool TilePlacementValid(int tileX, int tileY)
{
return tileX >= 0 && tileX < Main.maxTilesX && tileY >= 0 && tileY < Main.maxTilesY;
}
///
/// Checks if the tile is solid.
///
/// Location X
/// Location Y
/// The tile's solidity.
public bool TileSolid(int tileX, int tileY)
{
return TilePlacementValid(tileX, tileY) && Main.tile[tileX, tileY] != null &&
Main.tile[tileX, tileY].active() && Main.tileSolid[Main.tile[tileX, tileY].type] &&
!Main.tile[tileX, tileY].inActive() && !Main.tile[tileX, tileY].halfBrick() &&
Main.tile[tileX, tileY].slope() == 0 && Main.tile[tileX, tileY].type != TileID.Bubble;
}
///
/// Gets a list of items by ID, Name or Tag.
///
/// Item ID, Name or Tag.
/// A list of matching items.
public List GetItemByIdOrName(string text)
{
int type = -1;
if (Int32.TryParse(text, out type))
{
if (type >= Main.maxItemTypes)
return new List();
return new List {GetItemById(type)};
}
Item item = GetItemFromTag(text);
if (item != null)
return new List() { item };
return GetItemByName(text);
}
///
/// Gets an item by ID
///
/// ID
/// Item
public Item GetItemById(int id)
{
Item item = new Item();
item.netDefaults(id);
return item;
}
///
/// Gets items by name
///
/// name
/// List of Items
public List GetItemByName(string name)
{
var found = new List();
Item item = new Item();
string nameLower = name.ToLower();
for (int i = -48; i < Main.maxItemTypes; i++)
{
item.netDefaults(i);
if (String.IsNullOrWhiteSpace(item.name))
continue;
if (item.name.ToLower() == nameLower)
return new List { item };
if (item.name.ToLower().StartsWith(nameLower))
found.Add(item.Clone());
}
return found;
}
///
/// Gets an item based on a chat item tag.
///
/// A tag in the [i/s#/p#:netid] format.
/// The item represented by the tag.
public Item GetItemFromTag(string tag)
{
Regex regex = new Regex(@"\[i(tem)?(?:\/s(?\d{1,3}))?(?:\/p(?\d{1,3}))?:(?-?\d{1,4})\]");
Match match = regex.Match(tag);
if (!match.Success)
return null;
Item item = new Item();
item.netDefaults(Int32.Parse(match.Groups["NetID"].Value));
if (!String.IsNullOrWhiteSpace(match.Groups["Stack"].Value))
item.stack = Int32.Parse(match.Groups["Stack"].Value);
if (!String.IsNullOrWhiteSpace(match.Groups["Prefix"].Value))
item.prefix = Byte.Parse(match.Groups["Prefix"].Value);
return item;
}
///
/// Gets an NPC by ID or Name
///
///
/// List of NPCs
public List GetNPCByIdOrName(string idOrName)
{
int type = -1;
if (int.TryParse(idOrName, out type))
{
if (type >= Main.maxNPCTypes)
return new List();
return new List { GetNPCById(type) };
}
return GetNPCByName(idOrName);
}
///
/// Gets an NPC by ID
///
/// ID
/// NPC
public NPC GetNPCById(int id)
{
NPC npc = new NPC();
npc.netDefaults(id);
return npc;
}
///
/// Gets a NPC by name
///
/// Name
/// List of matching NPCs
public List GetNPCByName(string name)
{
var found = new List();
NPC npc = new NPC();
string nameLower = name.ToLower();
for (int i = -17; i < Main.maxNPCTypes; i++)
{
npc.netDefaults(i);
if (npc.name.ToLower() == nameLower)
return new List { npc };
if (npc.name.ToLower().StartsWith(nameLower))
found.Add((NPC)npc.Clone());
}
return found;
}
///
/// Gets a buff name by id
///
/// ID
/// name
public string GetBuffName(int id)
{
return (id > 0 && id < Main.maxBuffTypes) ? Main.buffName[id] : "null";
}
///
/// Gets the description of a buff
///
/// ID
/// description
public string GetBuffDescription(int id)
{
return (id > 0 && id < Main.maxBuffTypes) ? Main.buffTip[id] : "null";
}
///
/// Gets a list of buffs by name
///
/// name
/// Matching list of buff ids
public List GetBuffByName(string name)
{
string nameLower = name.ToLower();
string buffname;
for (int i = 1; i < Main.maxBuffTypes; i++)
{
buffname = Main.buffName[i];
if (!String.IsNullOrWhiteSpace(buffname) && buffname.ToLower() == nameLower)
return new List {i};
}
var found = new List();
for (int i = 1; i < Main.maxBuffTypes; i++)
{
buffname = Main.buffName[i];
if (!String.IsNullOrWhiteSpace(buffname) && buffname.ToLower().StartsWith(nameLower))
found.Add(i);
}
return found;
}
///
/// Gets a prefix based on its id
///
/// ID
/// Prefix name
public string GetPrefixById(int id)
{
return id < FirstItemPrefix || id > LastItemPrefix ? "" : Lang.prefix[id] ?? "";
}
///
/// Gets a list of prefixes by name
///
/// Name
/// List of prefix IDs
public List GetPrefixByName(string name)
{
Item item = new Item();
item.SetDefaults(0);
string lowerName = name.ToLower();
var found = new List();
for (int i = FirstItemPrefix; i <= LastItemPrefix; i++)
{
item.prefix = (byte)i;
string prefixName = item.AffixName().Trim().ToLower();
if (prefixName == lowerName)
return new List() { i };
else if (prefixName.StartsWith(lowerName)) // Partial match
found.Add(i);
}
return found;
}
///
/// Gets a prefix by ID or name
///
/// ID or name
/// List of prefix IDs
public List GetPrefixByIdOrName(string idOrName)
{
int type = -1;
if (int.TryParse(idOrName, out type) && type >= FirstItemPrefix && type <= LastItemPrefix)
{
return new List {type};
}
return GetPrefixByName(idOrName);
}
///
/// Kicks all player from the server without checking for immunetokick permission.
///
/// string reason
public void ForceKickAll(string reason)
{
foreach (TSPlayer player in TShock.Players)
{
if (player != null && player.Active)
{
ForceKick(player, reason, false, true);
}
}
}
///
/// Stops the server after kicking all players with a reason message, and optionally saving the world
///
/// bool perform a world save before stop (default: true)
/// string reason (default: "Server shutting down!")
public void StopServer(bool save = true, string reason = "Server shutting down!")
{
ForceKickAll(reason);
if (save)
SaveManager.Instance.SaveWorld();
// Save takes a while so kick again
ForceKickAll(reason);
// Broadcast so console can see we are shutting down as well
TShock.Utils.Broadcast(reason, Color.Red);
// Disconnect after kick as that signifies server is exiting and could cause a race
Netplay.disconnect = true;
}
///
/// Stops the server after kicking all players with a reason message, and optionally saving the world then attempts to
/// restart it.
///
/// bool perform a world save before stop (default: true)
/// string reason (default: "Server shutting down!")
public void RestartServer(bool save = true, string reason = "Server shutting down!")
{
if (Main.ServerSideCharacter)
foreach (TSPlayer player in TShock.Players)
if (player != null && player.IsLoggedIn && !player.IgnoreActionsForClearingTrashCan)
TShock.CharacterDB.InsertPlayerData(player);
StopServer(true, reason);
System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase);
Environment.Exit(0);
}
///
/// Reloads all configuration settings, groups, regions and raises the reload event.
///
public void Reload(TSPlayer player)
{
FileTools.SetupConfig();
TShock.HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs());
TShock.Groups.LoadPermisions();
TShock.Regions.Reload();
TShock.Itembans.UpdateItemBans();
TShock.ProjectileBans.UpdateBans();
TShock.TileBans.UpdateBans();
Hooks.GeneralHooks.OnReloadEvent(player);
}
///
/// Kicks a player from the server without checking for immunetokick permission.
///
/// TSPlayer player
/// string reason
/// bool silent (default: false)
/// bool saveSSI (default: false)
public void ForceKick(TSPlayer player, string reason, bool silent = false, bool saveSSI = false)
{
Kick(player, reason, true, silent, null, saveSSI);
}
///
/// Kicks a player from the server..
///
/// TSPlayer player
/// string reason
/// bool force (default: false)
/// bool silent (default: false)
/// string adminUserName (default: null)
/// bool saveSSI (default: false)
public bool Kick(TSPlayer player, string reason, bool force = false, bool silent = false, string adminUserName = null, bool saveSSI = false)
{
if (!player.ConnectionAlive)
return true;
if (force || !player.HasPermission(Permissions.immunetokick))
{
string playerName = player.Name;
player.SilentKickInProgress = silent;
if (player.IsLoggedIn && saveSSI)
player.SaveServerCharacter();
player.Disconnect(string.Format("Kicked: {0}", reason));
TShock.Log.ConsoleInfo(string.Format("Kicked {0} for : '{1}'", playerName, reason));
string verb = force ? "force " : "";
if (!silent)
{
if (string.IsNullOrWhiteSpace(adminUserName))
Broadcast(string.Format("{0} was {1}kicked for '{2}'", playerName, verb, reason.ToLower()), Color.Green);
else
Broadcast(string.Format("{0} {1}kicked {2} for '{3}'", adminUserName, verb, playerName, reason.ToLower()), Color.Green);
}
return true;
}
return false;
}
///
/// Bans and kicks a player from the server.
///
/// TSPlayer player
/// string reason
/// bool force (default: false)
/// string adminUserName (default: null)
public bool Ban(TSPlayer player, string reason, bool force = false, string adminUserName = null)
{
if (!player.ConnectionAlive)
return true;
if (force || !player.HasPermission(Permissions.immunetoban))
{
string ip = player.IP;
string uuid = player.UUID;
string playerName = player.Name;
TShock.Bans.AddBan(ip, playerName, uuid, reason, false, adminUserName);
player.Disconnect(string.Format("Banned: {0}", reason));
string verb = force ? "force " : "";
if (string.IsNullOrWhiteSpace(adminUserName))
TSPlayer.All.SendInfoMessage("{0} was {1}banned for '{2}'.", playerName, verb, reason);
else
TSPlayer.All.SendInfoMessage("{0} {1}banned {2} for '{3}'.", adminUserName, verb, playerName, reason);
return true;
}
return false;
}
/// HasBanExpired - Returns whether or not a ban has expired or not.
/// ban - The ban object to check.
/// byName - Defines whether or not the ban should be checked by name.
/// bool - True if the ban has expired.
public bool HasBanExpired(Ban ban, bool byName = false)
{
DateTime exp;
bool expirationExists = DateTime.TryParse(ban.Expiration, out exp);
if (!string.IsNullOrWhiteSpace(ban.Expiration) && (expirationExists) &&
(DateTime.UtcNow >= exp))
{
if (byName)
{
TShock.Bans.RemoveBan(ban.Name, true, true, false);
}
else
{
TShock.Bans.RemoveBan(ban.IP, false, false, false);
}
return true;
}
return false;
}
///
/// Shows a file to the user.
///
/// TSPlayer player
/// string filename reletave to savedir
public void ShowFileToUser(TSPlayer player, string file)
{
string foo = "";
using (var tr = new StreamReader(Path.Combine(TShock.SavePath, file)))
{
while ((foo = tr.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(foo))
{
continue;
}
foo = foo.Replace("%map%", (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName));
foo = foo.Replace("%players%", String.Join(",", GetPlayers(false)));
Regex reg = new Regex("%\\s*(?\\d{1,3})\\s*,\\s*(?\\d{1,3})\\s*,\\s*(?\\d{1,3})\\s*%");
var matches = reg.Matches(foo);
Color c = Color.White;
foreach (Match match in matches)
{
byte r, g, b;
if (byte.TryParse(match.Groups["r"].Value, out r) &&
byte.TryParse(match.Groups["g"].Value, out g) &&
byte.TryParse(match.Groups["b"].Value, out b))
{
c = new Color(r, g, b);
}
foo = foo.Remove(match.Index, match.Length);
}
player.SendMessage(foo, c);
}
}
}
///
/// Returns a Group from the name of the group
///
/// string groupName
public Group GetGroup(string groupName)
{
//first attempt on cached groups
for (int i = 0; i < TShock.Groups.groups.Count; i++)
{
if (TShock.Groups.groups[i].Name.Equals(groupName))
{
return TShock.Groups.groups[i];
}
}
return Group.DefaultGroup;
}
///
/// Returns an IPv4 address from a DNS query
///
/// string ip
public string GetIPv4Address(string hostname)
{
try
{
//Get the ipv4 address from GetHostAddresses, if an ip is passed it will return that ip
var ip = Dns.GetHostAddresses(hostname).FirstOrDefault(i => i.AddressFamily == AddressFamily.InterNetwork);
//if the dns query was successful then return it, otherwise return an empty string
return ip != null ? ip.ToString() : "";
}
catch (SocketException)
{
}
return "";
}
///
/// Sends the player an error message stating that more than one match was found
/// appending a csv list of the matches.
///
/// Player to send the message to
/// An enumerable list with the matches
public void SendMultipleMatchError(TSPlayer ply, IEnumerable