/* TShock, a server mod for Terraria Copyright (C) 2011-2017 Nyx Studios (fka. The TShock Team) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Data; using System.Linq; using MySql.Data.MySqlClient; using Terraria; using Microsoft.Xna.Framework; namespace TShockAPI.DB { public class RegionManager { /// /// The list of regions. /// public List Regions = new List(); private IDbConnection database; internal RegionManager(IDbConnection db) { database = db; var table = new SqlTable("Regions", new SqlColumn("Id", MySqlDbType.Int32) {Primary = true, AutoIncrement = true}, new SqlColumn("X1", MySqlDbType.Int32), new SqlColumn("Y1", MySqlDbType.Int32), new SqlColumn("width", MySqlDbType.Int32), new SqlColumn("height", MySqlDbType.Int32), new SqlColumn("RegionName", MySqlDbType.VarChar, 50) {Unique = true}, new SqlColumn("WorldID", MySqlDbType.VarChar, 50) { Unique = true }, new SqlColumn("UserIds", MySqlDbType.Text), new SqlColumn("Protected", MySqlDbType.Int32), new SqlColumn("Groups", MySqlDbType.Text), new SqlColumn("Owner", MySqlDbType.VarChar, 50), new SqlColumn("Z", MySqlDbType.Int32){ DefaultValue = "0" } ); var creator = new SqlTableCreator(db, db.GetSqlType() == SqlType.Sqlite ? (IQueryBuilder) new SqliteQueryCreator() : new MysqlQueryCreator()); creator.EnsureTableStructure(table); } /// /// Reloads all regions. /// public void Reload() { try { using (var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString())) { Regions.Clear(); while (reader.Read()) { int id = reader.Get("Id"); int X1 = reader.Get("X1"); int Y1 = reader.Get("Y1"); int height = reader.Get("height"); int width = reader.Get("width"); int Protected = reader.Get("Protected"); string mergedids = reader.Get("UserIds"); string name = reader.Get("RegionName"); string owner = reader.Get("Owner"); string groups = reader.Get("Groups"); int z = reader.Get("Z"); string[] splitids = mergedids.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); Region r = new Region(id, new Rectangle(X1, Y1, width, height), name, owner, Protected != 0, Main.worldID.ToString(), z); r.SetAllowedGroups(groups); try { for (int i = 0; i < splitids.Length; i++) { int userid; if (Int32.TryParse(splitids[i], out userid)) // if unparsable, it's not an int, so silently skip r.AllowedIDs.Add(userid); else TShock.Log.Warn("One of your UserIDs is not a usable integer: " + splitids[i]); } } catch (Exception e) { TShock.Log.Error("Your database contains invalid UserIDs (they should be ints)."); TShock.Log.Error("A lot of things will fail because of this. You must manually delete and re-create the allowed field."); TShock.Log.Error(e.ToString()); TShock.Log.Error(e.StackTrace); } Regions.Add(r); } } } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } } /// /// Adds a region to the database. /// /// TileX of the top left corner. /// TileY of the top left corner. /// Width of the region in tiles. /// Height of the region in tiles. /// The name of the region. /// The User Account Name of the person who created this region. /// The world id that this region is in. /// The Z index of the region. /// Whether the region was created and added successfully. public bool AddRegion(int tx, int ty, int width, int height, string regionname, string owner, string worldid, int z = 0) { if (GetRegionByName(regionname) != null) { return false; } try { database.Query( "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, Groups, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); int id; using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid)) { if (res.Read()) { id = res.Get("Id"); } else { return false; } } Region region = new Region(id, new Rectangle(tx, ty, width, height), regionname, owner, true, worldid, z); Regions.Add(region); Hooks.RegionHooks.OnRegionCreated(region); return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Delets the region from this world with a given ID. /// /// The ID of the region to delete. /// Whether the region was successfully deleted. public bool DeleteRegion(int id) { try { database.Query("DELETE FROM Regions WHERE Id=@0 AND WorldID=@1", id, Main.worldID.ToString()); var worldid = Main.worldID.ToString(); var region = Regions.FirstOrDefault(r => r.ID == id && r.WorldID == worldid); Regions.RemoveAll(r => r.ID == id && r.WorldID == worldid); Hooks.RegionHooks.OnRegionDeleted(region); return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Deletes the region from this world with a given name. /// /// The name of the region to delete. /// Whether the region was successfully deleted. public bool DeleteRegion(string name) { try { database.Query("DELETE FROM Regions WHERE RegionName=@0 AND WorldID=@1", name, Main.worldID.ToString()); var worldid = Main.worldID.ToString(); var region = Regions.FirstOrDefault(r => r.Name == name && r.WorldID == worldid); Regions.RemoveAll(r => r.Name == name && r.WorldID == worldid); Hooks.RegionHooks.OnRegionDeleted(region); return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Sets the protected state of the region with a given ID. /// /// The ID of the region to change. /// New protected state of the region. /// Whether the region's state was successfully changed. public bool SetRegionState(int id, bool state) { try { database.Query("UPDATE Regions SET Protected = @0 WHERE Id = @1 AND WorldID = @2", state ? 1 : 0, id, Main.worldID.ToString()); var region = GetRegionByID(id); if (region != null) { region.DisableBuild = state; } return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); return false; } } /// /// Sets the protected state of the region with a given name. /// /// The name of the region to change. /// New protected state of the region. /// Whether the region's state was successfully changed. public bool SetRegionState(string name, bool state) { try { database.Query("UPDATE Regions SET Protected=@0 WHERE RegionName=@1 AND WorldID=@2", state ? 1 : 0, name, Main.worldID.ToString()); var region = GetRegionByName(name); if (region != null) region.DisableBuild = state; return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); return false; } } /// /// Checks if a given player can build in a region at the given (x, y) coordinate /// /// X coordinate /// Y coordinate /// Player to check permissions with /// Whether the player can build at the given (x, y) coordinate public bool CanBuild(int x, int y, TSPlayer ply) { if (!ply.HasPermission(Permissions.canbuild)) { return false; } Region top = null; foreach (Region region in Regions.ToList()) { if (region.InArea(x, y)) { if (top == null || region.Z > top.Z) top = region; } } return top == null || top.HasPermissionToBuildInRegion(ply); } /// /// Checks if any regions exist at the given (x, y) coordinate /// /// X coordinate /// Y coordinate /// Whether any regions exist at the given (x, y) coordinate public bool InArea(int x, int y) { return Regions.Any(r => r.InArea(x, y)); } /// /// Checks if any regions exist at the given (x, y) coordinate /// and returns an IEnumerable containing their names /// /// X coordinate /// Y coordinate /// The names of any regions that exist at the given (x, y) coordinate public IEnumerable InAreaRegionName(int x, int y) { return Regions.Where(r => r.InArea(x, y)).Select(r => r.Name); } /// /// Checks if any regions exist at the given (x, y) coordinate /// and returns an IEnumerable containing their IDs /// /// X coordinate /// Y coordinate /// The IDs of any regions that exist at the given (x, y) coordinate public IEnumerable InAreaRegionID(int x, int y) { return Regions.Where(r => r.InArea(x, y)).Select(r => r.ID); } /// /// Checks if any regions exist at the given (x, y) coordinate /// and returns an IEnumerable containing their objects /// /// X coordinate /// Y coordinate /// The objects of any regions that exist at the given (x, y) coordinate public IEnumerable InAreaRegion(int x, int y) { return Regions.Where(r => r.InArea(x, y)); } /// /// Changes the size of a given region /// /// Name of the region to resize /// Amount to resize /// Direction to resize in: /// 0 = resize height and Y. /// 1 = resize width. /// 2 = resize height. /// 3 = resize width and X. /// public bool ResizeRegion(string regionName, int addAmount, int direction) { //0 = up //1 = right //2 = down //3 = left int X = 0; int Y = 0; int height = 0; int width = 0; try { using (var reader = database.QueryReader("SELECT X1, Y1, height, width FROM Regions WHERE RegionName=@0 AND WorldID=@1", regionName, Main.worldID.ToString())) { if (reader.Read()) { X = reader.Get("X1"); width = reader.Get("width"); Y = reader.Get("Y1"); height = reader.Get("height"); } } switch (direction) { case 0: Y -= addAmount; height += addAmount; break; case 1: width += addAmount; break; case 2: height += addAmount; break; case 3: X -= addAmount; width += addAmount; break; default: return false; } foreach (var region in Regions.Where(r => r.Name == regionName)) region.Area = new Rectangle(X, Y, width, height); int q = database.Query("UPDATE Regions SET X1 = @0, Y1 = @1, width = @2, height = @3 WHERE RegionName = @4 AND WorldID=@5", X, Y, width, height, regionName, Main.worldID.ToString()); if (q > 0) return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Removes an allowed user from a region /// /// Name of the region to modify /// Username to remove /// true if removed successfully public bool RemoveUser(string regionName, string userName) { Region r = GetRegionByName(regionName); if (r != null) { if (!r.RemoveID(TShock.Users.GetUserID(userName))) { return false; } string ids = string.Join(",", r.AllowedIDs); return database.Query("UPDATE Regions SET UserIds=@0 WHERE RegionName=@1 AND WorldID=@2", ids, regionName, Main.worldID.ToString()) > 0; } return false; } /// /// Adds a user to a region's allowed user list /// /// Name of the region to modify /// Username to add /// true if added successfully public bool AddNewUser(string regionName, string userName) { try { string mergedIDs = string.Empty; using ( var reader = database.QueryReader("SELECT UserIds FROM Regions WHERE RegionName=@0 AND WorldID=@1", regionName, Main.worldID.ToString())) { if (reader.Read()) mergedIDs = reader.Get("UserIds"); } string userIdToAdd = Convert.ToString(TShock.Users.GetUserID(userName)); string[] ids = mergedIDs.Split(','); // Is the user already allowed to the region? if (ids.Contains(userIdToAdd)) return true; if (string.IsNullOrEmpty(mergedIDs)) mergedIDs = userIdToAdd; else mergedIDs = string.Concat(mergedIDs, ",", userIdToAdd); int q = database.Query("UPDATE Regions SET UserIds=@0 WHERE RegionName=@1 AND WorldID=@2", mergedIDs, regionName, Main.worldID.ToString()); foreach (var r in Regions) { if (r.Name == regionName && r.WorldID == Main.worldID.ToString()) r.SetAllowedIDs(mergedIDs); } return q != 0; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Sets the position of a region. /// /// The region name. /// The X position. /// The Y position. /// The height. /// The width. /// Whether the operation succeeded. public bool PositionRegion(string regionName, int x, int y, int width, int height) { try { Region region = Regions.First(r => String.Equals(regionName, r.Name, StringComparison.OrdinalIgnoreCase)); region.Area = new Rectangle(x, y, width, height); if (database.Query("UPDATE Regions SET X1 = @0, Y1 = @1, width = @2, height = @3 WHERE RegionName = @4 AND WorldID = @5", x, y, width, height, regionName, Main.worldID.ToString()) > 0) return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } /// /// Gets all the regions names from world /// /// World name to get regions from /// List of regions with only their names public List ListAllRegions(string worldid) { var regions = new List(); try { using (var reader = database.QueryReader("SELECT RegionName FROM Regions WHERE WorldID=@0", worldid)) { while (reader.Read()) regions.Add(new Region {Name = reader.Get("RegionName")}); } } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return regions; } /// /// Returns a region with the given name /// /// Region name /// The region with the given name, or null if not found public Region GetRegionByName(String name) { return Regions.FirstOrDefault(r => r.Name.Equals(name) && r.WorldID == Main.worldID.ToString()); } /// /// Returns a region with the given ID /// /// Region ID /// The region with the given ID, or null if not found public Region GetRegionByID(int id) { return Regions.FirstOrDefault(r => r.ID == id && r.WorldID == Main.worldID.ToString()); } /// /// Changes the owner of the region with the given name /// /// Region name /// New owner's username /// Whether the change was successfull public bool ChangeOwner(string regionName, string newOwner) { var region = GetRegionByName(regionName); if (region != null) { region.Owner = newOwner; int q = database.Query("UPDATE Regions SET Owner=@0 WHERE RegionName=@1 AND WorldID=@2", newOwner, regionName, Main.worldID.ToString()); if (q > 0) return true; } return false; } /// /// Allows a group to use a region /// /// Region name /// Group's name /// Whether the change was successfull public bool AllowGroup(string regionName, string groupName) { string mergedGroups = ""; using ( var reader = database.QueryReader("SELECT Groups FROM Regions WHERE RegionName=@0 AND WorldID=@1", regionName, Main.worldID.ToString())) { if (reader.Read()) mergedGroups = reader.Get("Groups"); } string[] groups = mergedGroups.Split(','); // Is the group already allowed to the region? if (groups.Contains(groupName)) return true; if (mergedGroups != "") mergedGroups += ","; mergedGroups += groupName; int q = database.Query("UPDATE Regions SET Groups=@0 WHERE RegionName=@1 AND WorldID=@2", mergedGroups, regionName, Main.worldID.ToString()); Region r = GetRegionByName(regionName); if (r != null) { r.SetAllowedGroups(mergedGroups); } else { return false; } return q > 0; } /// /// Removes a group's access to a region /// /// Region name /// Group name /// Whether the change was successfull public bool RemoveGroup(string regionName, string group) { Region r = GetRegionByName(regionName); if (r != null) { r.RemoveGroup(group); string groups = string.Join(",", r.AllowedGroups); int q = database.Query("UPDATE Regions SET Groups=@0 WHERE RegionName=@1 AND WorldID=@2", groups, regionName, Main.worldID.ToString()); if (q > 0) return true; } return false; } /// /// Returns the with the highest Z index of the given list /// /// List of Regions to compare /// public Region GetTopRegion(IEnumerable regions) { Region ret = null; foreach (Region r in regions) { if (ret == null) ret = r; else { if (r.Z > ret.Z) ret = r; } } return ret; } /// /// Sets the Z index of a given region /// /// Region name /// New Z index /// Whether the change was successfull public bool SetZ(string name, int z) { try { database.Query("UPDATE Regions SET Z=@0 WHERE RegionName=@1 AND WorldID=@2", z, name, Main.worldID.ToString()); var region = GetRegionByName(name); if (region != null) region.Z = z; return true; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); return false; } } } public class Region { public int ID { get; set; } public Rectangle Area { get; set; } public string Name { get; set; } public string Owner { get; set; } public bool DisableBuild { get; set; } public string WorldID { get; set; } public List AllowedIDs { get; set; } public List AllowedGroups { get; set; } public int Z { get; set; } public Region(int id, Rectangle region, string name, string owner, bool disablebuild, string RegionWorldIDz, int z) : this() { ID = id; Area = region; Name = name; Owner = owner; DisableBuild = disablebuild; WorldID = RegionWorldIDz; Z = z; } public Region() { Area = Rectangle.Empty; Name = string.Empty; DisableBuild = true; WorldID = string.Empty; AllowedIDs = new List(); AllowedGroups = new List(); Z = 0; } /// /// Checks if a given point is in the region's area /// /// Point to check /// Whether the point exists in the region's area public bool InArea(Rectangle point) { return InArea(point.X, point.Y); } /// /// Checks if a given (x, y) coordinate is in the region's area /// /// X coordinate to check /// Y coordinate to check /// Whether the coordinate exists in the region's area public bool InArea(int x, int y) //overloaded with x,y { /* DO NOT CHANGE TO Area.Contains(x, y)! Area.Contains does not account for the right and bottom 'border' of the rectangle, which results in regions being trimmed. */ return x >= Area.X && x <= Area.X + Area.Width && y >= Area.Y && y <= Area.Y + Area.Height; } /// /// Checks if a given player has permission to build in the region /// /// Player to check permissions with /// Whether the player has permission public bool HasPermissionToBuildInRegion(TSPlayer ply) { if (!DisableBuild) { return true; } if (!ply.IsLoggedIn) { if (!ply.HasBeenNaggedAboutLoggingIn) { ply.SendMessage("You must be logged in to take advantage of protected regions.", Color.Red); ply.HasBeenNaggedAboutLoggingIn = true; } return false; } return ply.HasPermission(Permissions.editregion) || AllowedIDs.Contains(ply.User.ID) || AllowedGroups.Contains(ply.Group.Name) || Owner == ply.User.Name; } /// /// Sets the user IDs which are allowed to use the region /// /// String of IDs to set public void SetAllowedIDs(String ids) { String[] idArr = ids.Split(','); List idList = new List(); foreach (String id in idArr) { int i = 0; if (int.TryParse(id, out i) && i != 0) { idList.Add(i); } } AllowedIDs = idList; } /// /// Sets the group names which are allowed to use the region /// /// String of group names to set public void SetAllowedGroups(String groups) { // prevent null pointer exceptions if (!string.IsNullOrEmpty(groups)) { List groupList = groups.Split(',').ToList(); for (int i = 0; i < groupList.Count; i++) { groupList[i] = groupList[i].Trim(); } AllowedGroups = groupList; } } /// /// Removes a user's access to the region /// /// User ID to remove /// true if the user was found and removed from the region's allowed users public bool RemoveID(int id) { return AllowedIDs.Remove(id); } /// /// Removes a group's access to the region /// /// Group name to remove /// public bool RemoveGroup(string groupName) { return AllowedGroups.Remove(groupName); } } }