Merge branch 'general-devel' into patch-1

This commit is contained in:
koneko-nyan 2017-10-13 09:30:08 +02:00 committed by GitHub
commit 6606c7a7b7
8 changed files with 236 additions and 25 deletions

11
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,11 @@
## TShock version
## Any stack traces that may have happened when the issue occurred
## Steps to reproduce
## Screenshots of the problem, if applicable

View file

@ -4,6 +4,7 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin
## Upcoming Changes ## Upcoming Changes
* API: Added hooks for item, projectile and tile bans (@deadsurgeon42) * API: Added hooks for item, projectile and tile bans (@deadsurgeon42)
* API: Changed `PlayerHooks` permission hook mechanisms to allow negation from hooks (@deadsurgeon42)
* API: New WorldGrassSpread hook which shold allow corruption/crimson/hallow creep config options to work (@DeathCradle) * API: New WorldGrassSpread hook which shold allow corruption/crimson/hallow creep config options to work (@DeathCradle)
* Fixed saving when one player is one the server and another one joins (@MarioE) * Fixed saving when one player is one the server and another one joins (@MarioE)
* Fixed /spawnmob not spawning negative IDs (@MarioE) * Fixed /spawnmob not spawning negative IDs (@MarioE)
@ -11,10 +12,11 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin
* Updated to new stat tracking system with more data so we can actually make informed software decisions (Jordan Coulam) * Updated to new stat tracking system with more data so we can actually make informed software decisions (Jordan Coulam)
* Fixed /time display at the end of Terraria hours (@koneko-nyan) * Fixed /time display at the end of Terraria hours (@koneko-nyan)
* Added a warning notifying users of the minimum memory required to run TShock (@bartico6) * Added a warning notifying users of the minimum memory required to run TShock (@bartico6)
* Added /group rename to allow changing group names (@ColinBohn, @ProfessorXZ)
* Added /region rename and OnRegionRenamed hook (@koneko-nyan, @deadsurgeon42) * Added /region rename and OnRegionRenamed hook (@koneko-nyan, @deadsurgeon42)
## TShock 4.3.24 ## TShock 4.3.24
* API: Changed `PlayerHooks` permission hook mechanisms to allow negation from hooks (@deadsurgeon42)
* Updated OpenTerraria API to 1.3.5.3 (@DeathCradle) * Updated OpenTerraria API to 1.3.5.3 (@DeathCradle)
* Updated Terraria Server API to 1.3.5.3 (@WhiteXZ, @hakusaro) * Updated Terraria Server API to 1.3.5.3 (@WhiteXZ, @hakusaro)
* Updated TShock core components to 1.3.5.3 (@hakusaro) * Updated TShock core components to 1.3.5.3 (@hakusaro)

View file

@ -2865,6 +2865,7 @@ namespace TShockAPI
"add <name> <permissions...> - Adds a new group.", "add <name> <permissions...> - Adds a new group.",
"addperm <group> <permissions...> - Adds permissions to a group.", "addperm <group> <permissions...> - Adds permissions to a group.",
"color <group> <rrr,ggg,bbb> - Changes a group's chat color.", "color <group> <rrr,ggg,bbb> - Changes a group's chat color.",
"rename <group> <new name> - Changes a group's name.",
"del <group> - Deletes a group.", "del <group> - Deletes a group.",
"delperm <group> <permissions...> - Removes permissions from a group.", "delperm <group> <permissions...> - Removes permissions from a group.",
"list [page] - Lists groups.", "list [page] - Lists groups.",
@ -3074,6 +3075,29 @@ namespace TShockAPI
} }
#endregion #endregion
return; return;
case "rename":
#region Rename group
{
if (args.Parameters.Count != 3)
{
args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}group rename <group> <new name>", Specifier);
return;
}
string group = args.Parameters[1];
string newName = args.Parameters[2];
try
{
string response = TShock.Groups.RenameGroup(group, newName);
args.Player.SendSuccessMessage(response);
}
catch (GroupManagerException ex)
{
args.Player.SendErrorMessage(ex.Message);
}
}
#endregion
return;
case "del": case "del":
#region Delete group #region Delete group
{ {

View file

@ -26,27 +26,34 @@ using MySql.Data.MySqlClient;
namespace TShockAPI.DB namespace TShockAPI.DB
{ {
/// <summary>
/// Represents the GroupManager, which is in charge of group management.
/// </summary>
public class GroupManager : IEnumerable<Group> public class GroupManager : IEnumerable<Group>
{ {
private IDbConnection database; private IDbConnection database;
public readonly List<Group> groups = new List<Group>(); public readonly List<Group> groups = new List<Group>();
/// <summary>
/// Initializes a new instance of the <see cref="GroupManager"/> class with the specified database connection.
/// </summary>
/// <param name="db">The connection.</param>
public GroupManager(IDbConnection db) public GroupManager(IDbConnection db)
{ {
database = db; database = db;
var table = new SqlTable("GroupList", var table = new SqlTable("GroupList",
new SqlColumn("GroupName", MySqlDbType.VarChar, 32) {Primary = true}, new SqlColumn("GroupName", MySqlDbType.VarChar, 32) { Primary = true },
new SqlColumn("Parent", MySqlDbType.VarChar, 32), new SqlColumn("Parent", MySqlDbType.VarChar, 32),
new SqlColumn("Commands", MySqlDbType.Text), new SqlColumn("Commands", MySqlDbType.Text),
new SqlColumn("ChatColor", MySqlDbType.Text), new SqlColumn("ChatColor", MySqlDbType.Text),
new SqlColumn("Prefix", MySqlDbType.Text), new SqlColumn("Prefix", MySqlDbType.Text),
new SqlColumn("Suffix", MySqlDbType.Text) new SqlColumn("Suffix", MySqlDbType.Text)
); );
var creator = new SqlTableCreator(db, var creator = new SqlTableCreator(db,
db.GetSqlType() == SqlType.Sqlite db.GetSqlType() == SqlType.Sqlite
? (IQueryBuilder) new SqliteQueryCreator() ? (IQueryBuilder)new SqliteQueryCreator()
: new MysqlQueryCreator()); : new MysqlQueryCreator());
if (creator.EnsureTableStructure(table)) if (creator.EnsureTableStructure(table))
{ {
// Add default groups if they don't exist // Add default groups if they don't exist
@ -85,7 +92,11 @@ namespace TShockAPI.DB
AddGroup(name, parent, permissions, Group.defaultChatColor); AddGroup(name, parent, permissions, Group.defaultChatColor);
} }
/// <summary>
/// Determines whether the given group exists.
/// </summary>
/// <param name="group">The group.</param>
/// <returns><c>true</c> if it does; otherwise, <c>false</c>.</returns>
public bool GroupExists(string group) public bool GroupExists(string group)
{ {
if (group == "superadmin") if (group == "superadmin")
@ -99,11 +110,20 @@ namespace TShockAPI.DB
return GetEnumerator(); return GetEnumerator();
} }
/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
public IEnumerator<Group> GetEnumerator() public IEnumerator<Group> GetEnumerator()
{ {
return groups.GetEnumerator(); return groups.GetEnumerator();
} }
/// <summary>
/// Gets the group matching the specified name.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>The group.</returns>
public Group GetGroupByName(string name) public Group GetGroupByName(string name)
{ {
var ret = groups.Where(g => g.Name == name); var ret = groups.Where(g => g.Name == name);
@ -139,8 +159,8 @@ namespace TShockAPI.DB
} }
string query = (TShock.Config.StorageType.ToLower() == "sqlite") string query = (TShock.Config.StorageType.ToLower() == "sqlite")
? "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);" ? "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);"
: "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3"; : "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3";
if (database.Query(query, name, parentname, permissions, chatcolor) == 1) if (database.Query(query, name, parentname, permissions, chatcolor) == 1)
{ {
groups.Add(group); groups.Add(group);
@ -200,6 +220,114 @@ namespace TShockAPI.DB
group.Suffix = suffix; group.Suffix = suffix;
} }
/// <summary>
/// Renames the specified group.
/// </summary>
/// <param name="name">The group's name.</param>
/// <param name="newName">The new name.</param>
/// <returns>The result from the operation to be sent back to the user.</returns>
public String RenameGroup(string name, string newName)
{
if (!GroupExists(name))
{
throw new GroupNotExistException(name);
}
if (GroupExists(newName))
{
throw new GroupExistsException(newName);
}
using (var db = database.CloneEx())
{
db.Open();
using (var transaction = db.BeginTransaction())
{
try
{
using (var command = db.CreateCommand())
{
command.CommandText = "UPDATE GroupList SET GroupName = @0 WHERE GroupName = @1";
command.AddParameter("@0", newName);
command.AddParameter("@1", name);
command.ExecuteNonQuery();
}
var oldGroup = GetGroupByName(name);
var newGroup = new Group(newName, oldGroup.Parent, oldGroup.ChatColor, oldGroup.Permissions)
{
Prefix = oldGroup.Prefix,
Suffix = oldGroup.Suffix
};
groups.Remove(oldGroup);
groups.Add(newGroup);
// We need to check if the old group has been referenced as a parent and update those references accordingly
using (var command = db.CreateCommand())
{
command.CommandText = "UPDATE GroupList SET Parent = @0 WHERE Parent = @1";
command.AddParameter("@0", newName);
command.AddParameter("@1", name);
command.ExecuteNonQuery();
}
foreach (var group in groups.Where(g => g.Parent != null && g.Parent == oldGroup))
{
group.Parent = newGroup;
}
// Read the config file to prevent the possible loss of any unsaved changes
TShock.Config = ConfigFile.Read(FileTools.ConfigPath);
if (TShock.Config.DefaultGuestGroupName == oldGroup.Name)
{
TShock.Config.DefaultGuestGroupName = newGroup.Name;
Group.DefaultGroup = newGroup;
}
if (TShock.Config.DefaultRegistrationGroupName == oldGroup.Name)
{
TShock.Config.DefaultRegistrationGroupName = newGroup.Name;
}
TShock.Config.Write(FileTools.ConfigPath);
// We also need to check if any users belong to the old group and automatically apply changes
using (var command = db.CreateCommand())
{
command.CommandText = "UPDATE Users SET Usergroup = @0 WHERE Usergroup = @1";
command.AddParameter("@0", newName);
command.AddParameter("@1", name);
command.ExecuteNonQuery();
}
foreach (var player in TShock.Players.Where(p => p?.Group == oldGroup))
{
player.Group = newGroup;
}
transaction.Commit();
return $"Group \"{name}\" has been renamed to \"{newName}\".";
}
catch (Exception ex)
{
TShock.Log.Error($"An exception has occured during database transaction: {ex.Message}");
try
{
transaction.Rollback();
}
catch (Exception rollbackEx)
{
TShock.Log.Error($"An exception has occured during database rollback: {rollbackEx.Message}");
}
}
}
}
throw new GroupManagerException($"Failed to rename group \"{name}\".");
}
/// <summary>
/// Deletes the specified group.
/// </summary>
/// <param name="name">The group's name.</param>
/// <param name="exceptions">Whether exceptions will be thrown in case something goes wrong.</param>
/// <returns>The result from the operation to be sent back to the user.</returns>
public String DeleteGroup(String name, bool exceptions = false) public String DeleteGroup(String name, bool exceptions = false)
{ {
if (!GroupExists(name)) if (!GroupExists(name))
@ -214,12 +342,18 @@ namespace TShockAPI.DB
groups.Remove(TShock.Utils.GetGroup(name)); groups.Remove(TShock.Utils.GetGroup(name));
return "Group " + name + " has been deleted successfully."; return "Group " + name + " has been deleted successfully.";
} }
else if (exceptions)
throw new GroupManagerException("Failed to delete group '" + name + ".'");
return ""; if (exceptions)
throw new GroupManagerException("Failed to delete group '" + name + ".'");
return "Failed to delete group '" + name + ".'";
} }
/// <summary>
/// Enumerates the given permission list and adds permissions for the specified group accordingly.
/// </summary>
/// <param name="name">The group name.</param>
/// <param name="permissions">The permission list.</param>
/// <returns>The result from the operation to be sent back to the user.</returns>
public String AddPermissions(String name, List<String> permissions) public String AddPermissions(String name, List<String> permissions)
{ {
if (!GroupExists(name)) if (!GroupExists(name))
@ -237,6 +371,12 @@ namespace TShockAPI.DB
return ""; return "";
} }
/// <summary>
/// Enumerates the given permission list and removes valid permissions for the specified group accordingly.
/// </summary>
/// <param name="name">The group name.</param>
/// <param name="permissions">The permission list.</param>
/// <returns>The result from the operation to be sent back to the user.</returns>
public String DeletePermissions(String name, List<String> permissions) public String DeletePermissions(String name, List<String> permissions)
{ {
if (!GroupExists(name)) if (!GroupExists(name))
@ -254,12 +394,15 @@ namespace TShockAPI.DB
return ""; return "";
} }
/// <summary>
/// Enumerates the group list and loads permissions for each group appropriately.
/// </summary>
public void LoadPermisions() public void LoadPermisions()
{ {
try try
{ {
List<Group> newGroups = new List<Group>(groups.Count); List<Group> newGroups = new List<Group>(groups.Count);
Dictionary<string,string> newGroupParents = new Dictionary<string, string>(groups.Count); Dictionary<string, string> newGroupParents = new Dictionary<string, string>(groups.Count);
using (var reader = database.QueryReader("SELECT * FROM GroupList")) using (var reader = database.QueryReader("SELECT * FROM GroupList"))
{ {
while (reader.Read()) while (reader.Read())
@ -271,7 +414,8 @@ namespace TShockAPI.DB
continue; continue;
} }
newGroups.Add(new Group(groupName, null, reader.Get<string>("ChatColor"), reader.Get<string>("Commands")) { newGroups.Add(new Group(groupName, null, reader.Get<string>("ChatColor"), reader.Get<string>("Commands"))
{
Prefix = reader.Get<string>("Prefix"), Prefix = reader.Get<string>("Prefix"),
Suffix = reader.Get<string>("Suffix"), Suffix = reader.Get<string>("Suffix"),
}); });
@ -360,32 +504,60 @@ namespace TShockAPI.DB
} }
} }
/// <summary>
/// Represents the base GroupManager exception.
/// </summary>
[Serializable] [Serializable]
public class GroupManagerException : Exception public class GroupManagerException : Exception
{ {
/// <summary>
/// Initializes a new instance of the <see cref="GroupManagerException"/> with the specified message.
/// </summary>
/// <param name="message">The message.</param>
public GroupManagerException(string message) public GroupManagerException(string message)
: base(message) : base(message)
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="GroupManagerException"/> with the specified message and inner exception.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="inner">The inner exception.</param>
public GroupManagerException(string message, Exception inner) public GroupManagerException(string message, Exception inner)
: base(message, inner) : base(message, inner)
{ {
} }
} }
/// <summary>
/// Represents the GroupExists exception.
/// This exception is thrown whenever an attempt to add an existing group into the database is made.
/// </summary>
[Serializable] [Serializable]
public class GroupExistsException : GroupManagerException public class GroupExistsException : GroupManagerException
{ {
/// <summary>
/// Initializes a new instance of the <see cref="GroupExistsException"/> with the specified group name.
/// </summary>
/// <param name="name">The group name.</param>
public GroupExistsException(string name) public GroupExistsException(string name)
: base("Group '" + name + "' already exists") : base("Group '" + name + "' already exists")
{ {
} }
} }
/// <summary>
/// Represents the GroupNotExist exception.
/// This exception is thrown whenever we try to access a group that does not exist.
/// </summary>
[Serializable] [Serializable]
public class GroupNotExistException : GroupManagerException public class GroupNotExistException : GroupManagerException
{ {
/// <summary>
/// Initializes a new instance of the <see cref="GroupNotExistException"/> with the specified group name.
/// </summary>
/// <param name="name">The group name.</param>
public GroupNotExistException(string name) public GroupNotExistException(string name)
: base("Group '" + name + "' does not exist") : base("Group '" + name + "' does not exist")
{ {

View file

@ -74,7 +74,9 @@
</Reference> </Reference>
<Reference Include="OTAPI, Version=1.3.4.4, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="OTAPI, Version=1.3.4.4, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\TerrariaServerAPI\TerrariaServerAPI\bin\Debug\OTAPI.dll</HintPath> <HintPath Condition="Exists('..\TerrariaServerAPI\TerrariaServerAPI\bin\Debug\OTAPI.dll')">..\TerrariaServerAPI\TerrariaServerAPI\bin\Debug\OTAPI.dll</HintPath>
<HintPath Condition="Exists('..\TerrariaServerAPI\TerrariaServerAPI\bin\Release\OTAPI.dll')">..\TerrariaServerAPI\TerrariaServerAPI\bin\Release\OTAPI.dll</HintPath>
<HintPath Condition="Exists('..\TerrariaServerAPI\TerrariaServerAPI\bin\$(Configuration)\OTAPI.dll')">..\TerrariaServerAPI\TerrariaServerAPI\bin\$(Configuration)\OTAPI.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />

View file

@ -145,7 +145,7 @@ def update_terraria_source():
def run_bootstrapper(): def run_bootstrapper():
for build_config in ['Debug','Release'] : for build_config in ['Debug','Release'] :
mintaka = subprocess.Popen(['xbuild', './TerrariaServerAPI/TShock.4.OTAPI.sln', '/p:Configuration=' + build_config]) mintaka = subprocess.Popen(['msbuild', './TerrariaServerAPI/TShock.4.OTAPI.sln', '/p:Configuration=' + build_config])
mintaka.wait() mintaka.wait()
@ -161,7 +161,7 @@ def run_bootstrapper():
if (bootstrapper_proc.returncode != 0): if (bootstrapper_proc.returncode != 0):
raise CalledProcessError(bootstrapper_proc.returncode) raise CalledProcessError(bootstrapper_proc.returncode)
tsapi_proc = subprocess.Popen(['xbuild', './TerrariaServerAPI/TerrariaServerAPI/TerrariaServerAPI.csproj', '/p:Configuration=' + build_config]) tsapi_proc = subprocess.Popen(['msbuild', './TerrariaServerAPI/TerrariaServerAPI/TerrariaServerAPI.csproj', '/p:Configuration=' + build_config])
tsapi_proc.wait() tsapi_proc.wait()
@ -169,8 +169,8 @@ def run_bootstrapper():
raise CalledProcessError(tsapi_proc.returncode) raise CalledProcessError(tsapi_proc.returncode)
def build_software(): def build_software():
release_proc = subprocess.Popen(['xbuild', './TShockAPI/TShockAPI.csproj', '/p:Configuration=Release']) release_proc = subprocess.Popen(['msbuild', './TShockAPI/TShockAPI.csproj', '/p:Configuration=Release'])
debug_proc = subprocess.Popen(['xbuild', './TShockAPI/TShockAPI.csproj', '/p:Configuration=Debug']) debug_proc = subprocess.Popen(['msbuild', './TShockAPI/TShockAPI.csproj', '/p:Configuration=Debug'])
release_proc.wait() release_proc.wait()
debug_proc.wait() debug_proc.wait()
if (release_proc.returncode != 0): if (release_proc.returncode != 0):