<commit_before>
// RankManager.cpp

// Implements the cRankManager class that represents the rank manager responsible for assigning permissions and message visuals to players

#include "Globals.h"
#include "RankManager.h"
#include "inifile/iniFile.h"
#include "Protocol/MojangAPI.h"
#include "ClientHandle.h"





////////////////////////////////////////////////////////////////////////////////
// cRankManagerIniMigrator:

/** Migrates from groups.ini and users.ini into the rankmanager DB */
class cRankManagerIniMigrator
{
public:
	cRankManagerIniMigrator(cRankManager & a_RankManager, cMojangAPI & a_MojangAPI) :
		m_RankManager(a_RankManager),
		m_MojangAPI(a_MojangAPI)
	{
	}
	
	
	
	/** Performs the complete migration from INI files to DB. */
	bool Migrate(void)
	{
		cRankManager::cMassChangeLock Lock(m_RankManager);
		
		LOGD("Reading groups...");
		if (!ReadGroups())
		{
			return false;
		}
		LOGD("Cleaning groups inheritance...");
		CleanGroupInheritance();
		LOGD("Creating groups...");
		CreateGroups();

		LOGD("Reading users...");
		if (!ReadUsers())
		{
			return false;
		}
		LOGD("Cleaning user groups...");
		CleanUserGroups();
		LOGD("Resolving user UUIDs...");
		ResolveUserUUIDs();
		
		LOGD("Setting ranks...");
		SetRanks();

		LOGD("Creating defaults...");
		CreateDefaults();
		
		return true;
	}
	
protected:

	/** Container for a group read from an INI file. */
	struct sGroup
	{
		AString m_Name;
		AString m_Color;
		AStringVector m_Inherits;
		AStringVector m_Permissions;
		
		sGroup(void) {}
		
		sGroup(const AString & a_Name, const AString & a_Color, const AStringVector & a_Inherits, const AStringVector & a_Permissions):
			m_Name(a_Name),
			m_Color(a_Color),
			m_Inherits(a_Inherits),
			m_Permissions(a_Permissions)
		{
		}
	};
	typedef std::map<AString, sGroup> sGroupMap;
	
	
	/** Container for a single user read from an INI file. */
	struct sUser
	{
		AString m_Name;
		AStringVector m_Groups;
		
		/** Assigned by ResolveUserUUIDs(), contains the online (Mojang) UUID of the player. */
		AString m_UUID;

		/** Assigned by ResolveUserUUIDs(), contains the offline (generated) UUID of the player. */
		AString m_OfflineUUID;
		
		
		sUser(void) {}
		
		sUser(const AString & a_Name, const AStringVector & a_Groups):
			m_Name(a_Name),
			m_Groups(a_Groups)
		{
		}
	};
	typedef std::map<AString, sUser> sUserMap;
	
	typedef std::map<AString, AString> cStringMap;
	
	
	/** The parent Rank manager where we will create the groups, ranks and players */
	cRankManager & m_RankManager;
	
	/** The player name to UUID resolver */
	cMojangAPI & m_MojangAPI;
	
	/** List of all groups read from the ini file */
	sGroupMap m_Groups;
	
	/** List of all players read from the ini file. */
	sUserMap  m_Users;
	
	/** Maps lists of groups to rank names.
	Each group list is either a simple "<Group>" if there's only one group,
	or "<PrimaryGroup>, <FirstSecondaryGroup>, <SecondSecondaryGroup>...", where the secondary groups are
	lowercased and  alpha-sorted. This makes the group lists comparable for equivalence, simply by comparing
	their string names.
	The ranks are named "<Group>" for single-group players, and "AutoMigratedRank_N" for the composite ranks,
	where N is a unique number. */
	cStringMap m_GroupsToRanks;

	
	
	/** Reads the groups from the "groups.ini" file into m_Groups */
	bool ReadGroups(void)
	{
		// Read the file:
		cIniFile Groups;
		if (!Groups.ReadFile("groups.ini"))
		{
			return false;
		}
		
		// Read all the groups into a map:
		int NumGroups = Groups.GetNumKeys();
		for (int i = 0; i < NumGroups; i++)
		{
			AString GroupName = Groups.GetKeyName(i);
			AString lcGroupName = StrToLower(GroupName);
			if (m_Groups.find(lcGroupName) != m_Groups.end())
			{
				LOGINFO("groups.ini contains a duplicate definition of group %s, ignoring the latter.", GroupName.c_str());
				continue;
			}
			m_Groups[lcGroupName] = sGroup(
				GroupName,
				Groups.GetValue(GroupName, "Color", ""),
				StringSplitAndTrim(Groups.GetValue(GroupName, "Inherits"), ","),
				StringSplitAndTrim(Groups.GetValue(GroupName, "Permissions"), ",")
			);
		}  // for i - Groups' keys
		return true;
	}
	

	
	/** Removes non-existent groups from all the groups' inheritance */
	void CleanGroupInheritance(void)
	{
		for (sGroupMap::iterator itrG = m_Groups.begin(), endG = m_Groups.end(); itrG != endG; ++itrG)
		{
			AStringVector & Inherits = itrG->second.m_Inherits;
			for (AStringVector::iterator itrI = Inherits.begin(); itrI != Inherits.end();)
			{
				AString lcInherits = StrToLower(*itrI);
				if (m_Groups.find(lcInherits) != m_Groups.end())
				{
					// Inherited group exists, continue checking the next one
					++itrI;
					continue;
				}
				// Inherited group doesn't exist, remove it from the list:
				LOGWARNING("RankMigrator: Group \"%s\" inherits from a non-existent group \"%s\", this inheritance will be ignored.",
					itrG->second.m_Name.c_str(), itrI->c_str()
				);
				AStringVector::iterator itrI2 = itrI;
				++itrI2;
				Inherits.erase(itrI);
				itrI = itrI2;
			}  // for itrI - Inherits[]
		}  // for itrG - m_Groups[]
	}

	
	
	/** Reads the users from the "users.ini" file into m_Users */
	bool ReadUsers(void)
	{
		// Read the file:
		cIniFile Users;
		if (!Users.ReadFile("users.ini"))
		{
			return false;
		}
		
		// Read all the users into a map:
		int NumUsers = Users.GetNumKeys();
		for (int i = 0; i < NumUsers; i++)
		{
			AString UserName = Users.GetKeyName(i);
			AString lcUserName = StrToLower(UserName);
			if (m_Users.find(lcUserName) != m_Users.end())
			{
				LOGINFO("users.ini contains a duplicate definition of user %s, ignoring the latter.", UserName.c_str());
				continue;
			}
			m_Users[lcUserName] = sUser(
				UserName,
				StringSplitAndTrim(Users.GetValue(UserName, "Groups", ""), ",")
			);
		}  // for i - Users' keys
		return true;
	}
	
	
	
	/** Removes non-existent groups from each user's definition. */
	void CleanUserGroups(void)
	{
		for (sUserMap::iterator itrU = m_Users.begin(), endU = m_Users.end(); itrU != endU; ++itrU)
		{
			AStringVector & Groups = itrU->second.m_Groups;
			for (AStringVector::iterator itrG = Groups.begin(); itrG != Groups.end();)
			{
				AString lcGroup = StrToLower(*itrG);
				if (m_Groups.find(lcGroup) != m_Groups.end())
				{
					// Assigned group exists, continue checking the next one
					++itrG;
					continue;
				}
				// Assigned  group doesn't exist, remove it from the list:
				LOGWARNING("RankMigrator: User \"%s\" is assigned a non-existent group \"%s\", this assignment will be ignored.",
					itrU->second.m_Name.c_str(), itrG->c_str()
				);
				AStringVector::iterator itrG2 = itrG;
				++itrG2;
				Groups.erase(itrG);
				itrG = itrG2;
			}  // for itrG - Groups[]
		}  // for itrU - m_Users[]
	}
	
	
	
	/** Creates groups based on m_Groups.
	Ignores group inheritance. */
	void CreateGroups(void)
	{
		// Create each group, with its permissions:
		for (sGroupMap::const_iterator itr = m_Groups.begin(), end = m_Groups.end(); itr != end; ++itr)
		{
			m_RankManager.AddGroup(itr->second.m_Name);
			m_RankManager.AddPermissionsToGroup(itr->second.m_Permissions, itr->second.m_Name);
		}  // for itr - m_Groups[]
	}
	
	
	/** Resolves the UUID of each user in m_Users.
	If a user doesn't resolve, they are removed and logged in the console. */
	void ResolveUserUUIDs(void)
	{
		// Resolve all PlayerNames at once (the API doesn't like single-name queries):
		AStringVector PlayerNames;
		for (sUserMap::const_iterator itr = m_Users.begin(), end = m_Users.end(); itr != end; ++itr)
		{
			PlayerNames.push_back(itr->second.m_Name);
		}
		m_MojangAPI.GetUUIDsFromPlayerNames(PlayerNames);
		
		// Assign the UUIDs back to players, remove those not resolved:
		for (sUserMap::iterator itr = m_Users.begin(); itr != m_Users.end(); ++itr)
		{
			AString UUID = m_MojangAPI.GetUUIDFromPlayerName(itr->second.m_Name);
			if (UUID.empty())
			{
				LOGWARNING("RankMigrator: Cannot resolve player %s to online UUID, player will be left unranked in online mode", itr->second.m_Name.c_str());
			}
			itr->second.m_UUID = UUID;
			itr->second.m_OfflineUUID = cClientHandle::GenerateOfflineUUID(itr->second.m_Name);
		}
	}
	
	
	
	/** Adds the specified groups to the specified ranks. Recurses on the groups' inheritance. */
	void AddGroupsToRank(const AStringVector & a_Groups, const AString & a_RankName)
	{
		for (AStringVector::const_iterator itr = a_Groups.begin(), end = a_Groups.end(); itr != end; ++itr)
		{
			// Normalize the group name:
			sGroup & Group = m_Groups[StrToLower(*itr)];
			
			// Avoid loops, check if the group is already added:
			if (m_RankManager.IsGroupInRank(Group.m_Name, a_RankName))
			{
				continue;
			}
			
			// Add the group, and all the groups it inherits from recursively:
			m_RankManager.AddGroupToRank(Group.m_Name, a_RankName);
			AddGroupsToRank(Group.m_Inherits, a_RankName);
		}  // for itr - a_Groups[]
	}
	
	
	
	/** Creates a rank for each player, based on the master groups they are assigned. */
	void SetRanks(void)
	{
		for (sUserMap::const_iterator itr = m_Users.begin(), end = m_Users.end(); itr != end; ++itr)
		{
			// Ignore users with no groups:
			const AStringVector & Groups = itr->second.m_Groups;
			if (Groups.empty())
			{
				LOGWARNING("RankMigrator: Player %s has no groups assigned to them, skipping the player.", itr->second.m_Name.c_str());
				continue;
			}
			
			// Compose the rank name out of group names:
			AString RankName;
			for (AStringVector::const_iterator itrG = Groups.begin(), endG = Groups.end(); itrG != endG; ++itrG)
			{
				AString GroupName = m_Groups[StrToLower(*itrG)].m_Name;  // Normalize group name
				if (!RankName.empty())
				{
					RankName.push_back(',');
				}
				RankName.append(GroupName);
			}  // for itrG - Groups[]
			
			// Create the rank, with al its groups:
			if (!m_RankManager.RankExists(RankName))
			{
				m_RankManager.AddRank(RankName, "", "", m_Groups[StrToLower(Groups[0])].m_Color);
				AddGroupsToRank(Groups, RankName);
			}
			
			// Set the rank to the user, using both the online and offline UUIDs:
			m_RankManager.SetPlayerRank(itr->second.m_UUID,        itr->second.m_Name, RankName);
			m_RankManager.SetPlayerRank(itr->second.m_OfflineUUID, itr->second.m_Name, RankName);
		}  // for itr - m_Users[]
	}



	/** Creates the Default rank that contains the Default group, if it exists.
	Sets the RankManager's default rank. */
	void CreateDefaults(void)
	{
		if (!m_RankManager.RankExists("Default"))
		{
			m_RankManager.AddRank("Default", "", "", "");
			if (!m_RankManager.IsGroupInRank("Default", "Default"))
			{
				m_RankManager.AddGroupToRank("Default", "Default");
			}
		}
		m_RankManager.SetDefaultRank("Default");
	}
};





////////////////////////////////////////////////////////////////////////////////
// cRankManager:

cRankManager::cRankManager(void) :
	m_DB("Ranks.sqlite", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE),
	m_IsInitialized(false),
	m_MojangAPI(NULL)
{
}





cRankManager::~cRankManager()
{
	if (m_MojangAPI != NULL)
	{
		m_MojangAPI->SetRankManager(NULL);
	}
}





void cRankManager::Initialize(cMojangAPI & a_MojangAPI)
{
	ASSERT(!m_IsInitialized);  // Calling Initialize for the second time?
	
	// Create the DB tables, if they don't exist:
	m_DB.exec("CREATE TABLE IF NOT EXISTS Rank (RankID INTEGER PRIMARY KEY, Name, MsgPrefix, MsgSuffix, MsgNameColorCode)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS PlayerRank (PlayerUUID, PlayerName, RankID INTEGER)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS PermGroup (PermGroupID INTEGER PRIMARY KEY, Name)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS RankPermGroup (RankID INTEGER, PermGroupID INTEGER)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS PermissionItem (PermGroupID INTEGER, Permission)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS DefaultRank (RankID INTEGER)");
	
	m_IsInitialized = true;
	
	a_MojangAPI.SetRankManager(this);

	// Check if tables empty, migrate from ini files then
	if (AreDBTablesEmpty())
	{
		LOGINFO("There are no ranks, migrating old-style INI files to new DB ranks...");
		LOGINFO("(This might take a while)");
		cRankManagerIniMigrator Migrator(*this, a_MojangAPI);
		if (Migrator.Migrate())
		{
			LOGINFO("Ranks migrated.");
			// The default rank has been set by the migrator
			return;
		}

		// Migration failed. Add some defaults
		LOGINFO("Rank migration failed, creating default ranks...");
		CreateDefaults();
		LOGINFO("Default ranks created.");
	}
	
	// Load the default rank:
	try
	{
		SQLite::Statement stmt(m_DB,
			"SELECT Rank.Name FROM Rank "
			"LEFT JOIN DefaultRank ON Rank.RankID = DefaultRank.RankID"
		);
		if (stmt.executeStep())
		{
			m_DefaultRank = stmt.getColumn(0).getText();
		}
	}
	catch (const SQLite::Exception & ex)
	{
		LOGWARNING("%s: Cannot load default rank: %s", __FUNCTION__, ex.what());
		return;
	}

	// If the default rank cannot be loaded, use the first rank:
	if (m_DefaultRank.empty())
	{
		SetDefaultRank(GetAllRanks()[0]);
	}
}





AString cRankManager::GetPlayerRankName(const AString & a_PlayerUUID)
{
	ASSERT(m_IsInitialized);
	cCSLock Lock(m_CS);
	
	try
	{
		SQLite::Statement stmt(m_DB, "SELECT Rank.Name FROM Rank LEFT JOIN PlayerRank ON Rank.RankID = PlayerRank.RankID WHERE PlayerRank.PlayerUUID = ?");
		stmt.bind(1, a_PlayerUUID);
		stmt.executeStep();
		if (stmt.isDone())
		{
			// No data returned from the DB
			return AString();
		}
		return stmt.getColumn(0).getText();
	}
	catch (const SQLite::Exception & ex)
	{
		LOGWARNING("%s: Cannot get player rank name: %s", __FUNCTION__, ex.what());
	}
	return AString();
}


<commit_msg>Removed isDone check<commit_after>
// RankManager.cpp

// Implements the cRankManager class that represents the rank manager responsible for assigning permissions and message visuals to players

#include "Globals.h"
#include "RankManager.h"
#include "inifile/iniFile.h"
#include "Protocol/MojangAPI.h"
#include "ClientHandle.h"





////////////////////////////////////////////////////////////////////////////////
// cRankManagerIniMigrator:

/** Migrates from groups.ini and users.ini into the rankmanager DB */
class cRankManagerIniMigrator
{
public:
	cRankManagerIniMigrator(cRankManager & a_RankManager, cMojangAPI & a_MojangAPI) :
		m_RankManager(a_RankManager),
		m_MojangAPI(a_MojangAPI)
	{
	}
	
	
	
	/** Performs the complete migration from INI files to DB. */
	bool Migrate(void)
	{
		cRankManager::cMassChangeLock Lock(m_RankManager);
		
		LOGD("Reading groups...");
		if (!ReadGroups())
		{
			return false;
		}
		LOGD("Cleaning groups inheritance...");
		CleanGroupInheritance();
		LOGD("Creating groups...");
		CreateGroups();

		LOGD("Reading users...");
		if (!ReadUsers())
		{
			return false;
		}
		LOGD("Cleaning user groups...");
		CleanUserGroups();
		LOGD("Resolving user UUIDs...");
		ResolveUserUUIDs();
		
		LOGD("Setting ranks...");
		SetRanks();

		LOGD("Creating defaults...");
		CreateDefaults();
		
		return true;
	}
	
protected:

	/** Container for a group read from an INI file. */
	struct sGroup
	{
		AString m_Name;
		AString m_Color;
		AStringVector m_Inherits;
		AStringVector m_Permissions;
		
		sGroup(void) {}
		
		sGroup(const AString & a_Name, const AString & a_Color, const AStringVector & a_Inherits, const AStringVector & a_Permissions):
			m_Name(a_Name),
			m_Color(a_Color),
			m_Inherits(a_Inherits),
			m_Permissions(a_Permissions)
		{
		}
	};
	typedef std::map<AString, sGroup> sGroupMap;
	
	
	/** Container for a single user read from an INI file. */
	struct sUser
	{
		AString m_Name;
		AStringVector m_Groups;
		
		/** Assigned by ResolveUserUUIDs(), contains the online (Mojang) UUID of the player. */
		AString m_UUID;

		/** Assigned by ResolveUserUUIDs(), contains the offline (generated) UUID of the player. */
		AString m_OfflineUUID;
		
		
		sUser(void) {}
		
		sUser(const AString & a_Name, const AStringVector & a_Groups):
			m_Name(a_Name),
			m_Groups(a_Groups)
		{
		}
	};
	typedef std::map<AString, sUser> sUserMap;
	
	typedef std::map<AString, AString> cStringMap;
	
	
	/** The parent Rank manager where we will create the groups, ranks and players */
	cRankManager & m_RankManager;
	
	/** The player name to UUID resolver */
	cMojangAPI & m_MojangAPI;
	
	/** List of all groups read from the ini file */
	sGroupMap m_Groups;
	
	/** List of all players read from the ini file. */
	sUserMap  m_Users;
	
	/** Maps lists of groups to rank names.
	Each group list is either a simple "<Group>" if there's only one group,
	or "<PrimaryGroup>, <FirstSecondaryGroup>, <SecondSecondaryGroup>...", where the secondary groups are
	lowercased and  alpha-sorted. This makes the group lists comparable for equivalence, simply by comparing
	their string names.
	The ranks are named "<Group>" for single-group players, and "AutoMigratedRank_N" for the composite ranks,
	where N is a unique number. */
	cStringMap m_GroupsToRanks;

	
	
	/** Reads the groups from the "groups.ini" file into m_Groups */
	bool ReadGroups(void)
	{
		// Read the file:
		cIniFile Groups;
		if (!Groups.ReadFile("groups.ini"))
		{
			return false;
		}
		
		// Read all the groups into a map:
		int NumGroups = Groups.GetNumKeys();
		for (int i = 0; i < NumGroups; i++)
		{
			AString GroupName = Groups.GetKeyName(i);
			AString lcGroupName = StrToLower(GroupName);
			if (m_Groups.find(lcGroupName) != m_Groups.end())
			{
				LOGINFO("groups.ini contains a duplicate definition of group %s, ignoring the latter.", GroupName.c_str());
				continue;
			}
			m_Groups[lcGroupName] = sGroup(
				GroupName,
				Groups.GetValue(GroupName, "Color", ""),
				StringSplitAndTrim(Groups.GetValue(GroupName, "Inherits"), ","),
				StringSplitAndTrim(Groups.GetValue(GroupName, "Permissions"), ",")
			);
		}  // for i - Groups' keys
		return true;
	}
	

	
	/** Removes non-existent groups from all the groups' inheritance */
	void CleanGroupInheritance(void)
	{
		for (sGroupMap::iterator itrG = m_Groups.begin(), endG = m_Groups.end(); itrG != endG; ++itrG)
		{
			AStringVector & Inherits = itrG->second.m_Inherits;
			for (AStringVector::iterator itrI = Inherits.begin(); itrI != Inherits.end();)
			{
				AString lcInherits = StrToLower(*itrI);
				if (m_Groups.find(lcInherits) != m_Groups.end())
				{
					// Inherited group exists, continue checking the next one
					++itrI;
					continue;
				}
				// Inherited group doesn't exist, remove it from the list:
				LOGWARNING("RankMigrator: Group \"%s\" inherits from a non-existent group \"%s\", this inheritance will be ignored.",
					itrG->second.m_Name.c_str(), itrI->c_str()
				);
				AStringVector::iterator itrI2 = itrI;
				++itrI2;
				Inherits.erase(itrI);
				itrI = itrI2;
			}  // for itrI - Inherits[]
		}  // for itrG - m_Groups[]
	}

	
	
	/** Reads the users from the "users.ini" file into m_Users */
	bool ReadUsers(void)
	{
		// Read the file:
		cIniFile Users;
		if (!Users.ReadFile("users.ini"))
		{
			return false;
		}
		
		// Read all the users into a map:
		int NumUsers = Users.GetNumKeys();
		for (int i = 0; i < NumUsers; i++)
		{
			AString UserName = Users.GetKeyName(i);
			AString lcUserName = StrToLower(UserName);
			if (m_Users.find(lcUserName) != m_Users.end())
			{
				LOGINFO("users.ini contains a duplicate definition of user %s, ignoring the latter.", UserName.c_str());
				continue;
			}
			m_Users[lcUserName] = sUser(
				UserName,
				StringSplitAndTrim(Users.GetValue(UserName, "Groups", ""), ",")
			);
		}  // for i - Users' keys
		return true;
	}
	
	
	
	/** Removes non-existent groups from each user's definition. */
	void CleanUserGroups(void)
	{
		for (sUserMap::iterator itrU = m_Users.begin(), endU = m_Users.end(); itrU != endU; ++itrU)
		{
			AStringVector & Groups = itrU->second.m_Groups;
			for (AStringVector::iterator itrG = Groups.begin(); itrG != Groups.end();)
			{
				AString lcGroup = StrToLower(*itrG);
				if (m_Groups.find(lcGroup) != m_Groups.end())
				{
					// Assigned group exists, continue checking the next one
					++itrG;
					continue;
				}
				// Assigned  group doesn't exist, remove it from the list:
				LOGWARNING("RankMigrator: User \"%s\" is assigned a non-existent group \"%s\", this assignment will be ignored.",
					itrU->second.m_Name.c_str(), itrG->c_str()
				);
				AStringVector::iterator itrG2 = itrG;
				++itrG2;
				Groups.erase(itrG);
				itrG = itrG2;
			}  // for itrG - Groups[]
		}  // for itrU - m_Users[]
	}
	
	
	
	/** Creates groups based on m_Groups.
	Ignores group inheritance. */
	void CreateGroups(void)
	{
		// Create each group, with its permissions:
		for (sGroupMap::const_iterator itr = m_Groups.begin(), end = m_Groups.end(); itr != end; ++itr)
		{
			m_RankManager.AddGroup(itr->second.m_Name);
			m_RankManager.AddPermissionsToGroup(itr->second.m_Permissions, itr->second.m_Name);
		}  // for itr - m_Groups[]
	}
	
	
	/** Resolves the UUID of each user in m_Users.
	If a user doesn't resolve, they are removed and logged in the console. */
	void ResolveUserUUIDs(void)
	{
		// Resolve all PlayerNames at once (the API doesn't like single-name queries):
		AStringVector PlayerNames;
		for (sUserMap::const_iterator itr = m_Users.begin(), end = m_Users.end(); itr != end; ++itr)
		{
			PlayerNames.push_back(itr->second.m_Name);
		}
		m_MojangAPI.GetUUIDsFromPlayerNames(PlayerNames);
		
		// Assign the UUIDs back to players, remove those not resolved:
		for (sUserMap::iterator itr = m_Users.begin(); itr != m_Users.end(); ++itr)
		{
			AString UUID = m_MojangAPI.GetUUIDFromPlayerName(itr->second.m_Name);
			if (UUID.empty())
			{
				LOGWARNING("RankMigrator: Cannot resolve player %s to online UUID, player will be left unranked in online mode", itr->second.m_Name.c_str());
			}
			itr->second.m_UUID = UUID;
			itr->second.m_OfflineUUID = cClientHandle::GenerateOfflineUUID(itr->second.m_Name);
		}
	}
	
	
	
	/** Adds the specified groups to the specified ranks. Recurses on the groups' inheritance. */
	void AddGroupsToRank(const AStringVector & a_Groups, const AString & a_RankName)
	{
		for (AStringVector::const_iterator itr = a_Groups.begin(), end = a_Groups.end(); itr != end; ++itr)
		{
			// Normalize the group name:
			sGroup & Group = m_Groups[StrToLower(*itr)];
			
			// Avoid loops, check if the group is already added:
			if (m_RankManager.IsGroupInRank(Group.m_Name, a_RankName))
			{
				continue;
			}
			
			// Add the group, and all the groups it inherits from recursively:
			m_RankManager.AddGroupToRank(Group.m_Name, a_RankName);
			AddGroupsToRank(Group.m_Inherits, a_RankName);
		}  // for itr - a_Groups[]
	}
	
	
	
	/** Creates a rank for each player, based on the master groups they are assigned. */
	void SetRanks(void)
	{
		for (sUserMap::const_iterator itr = m_Users.begin(), end = m_Users.end(); itr != end; ++itr)
		{
			// Ignore users with no groups:
			const AStringVector & Groups = itr->second.m_Groups;
			if (Groups.empty())
			{
				LOGWARNING("RankMigrator: Player %s has no groups assigned to them, skipping the player.", itr->second.m_Name.c_str());
				continue;
			}
			
			// Compose the rank name out of group names:
			AString RankName;
			for (AStringVector::const_iterator itrG = Groups.begin(), endG = Groups.end(); itrG != endG; ++itrG)
			{
				AString GroupName = m_Groups[StrToLower(*itrG)].m_Name;  // Normalize group name
				if (!RankName.empty())
				{
					RankName.push_back(',');
				}
				RankName.append(GroupName);
			}  // for itrG - Groups[]
			
			// Create the rank, with al its groups:
			if (!m_RankManager.RankExists(RankName))
			{
				m_RankManager.AddRank(RankName, "", "", m_Groups[StrToLower(Groups[0])].m_Color);
				AddGroupsToRank(Groups, RankName);
			}
			
			// Set the rank to the user, using both the online and offline UUIDs:
			m_RankManager.SetPlayerRank(itr->second.m_UUID,        itr->second.m_Name, RankName);
			m_RankManager.SetPlayerRank(itr->second.m_OfflineUUID, itr->second.m_Name, RankName);
		}  // for itr - m_Users[]
	}



	/** Creates the Default rank that contains the Default group, if it exists.
	Sets the RankManager's default rank. */
	void CreateDefaults(void)
	{
		if (!m_RankManager.RankExists("Default"))
		{
			m_RankManager.AddRank("Default", "", "", "");
			if (!m_RankManager.IsGroupInRank("Default", "Default"))
			{
				m_RankManager.AddGroupToRank("Default", "Default");
			}
		}
		m_RankManager.SetDefaultRank("Default");
	}
};





////////////////////////////////////////////////////////////////////////////////
// cRankManager:

cRankManager::cRankManager(void) :
	m_DB("Ranks.sqlite", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE),
	m_IsInitialized(false),
	m_MojangAPI(NULL)
{
}





cRankManager::~cRankManager()
{
	if (m_MojangAPI != NULL)
	{
		m_MojangAPI->SetRankManager(NULL);
	}
}





void cRankManager::Initialize(cMojangAPI & a_MojangAPI)
{
	ASSERT(!m_IsInitialized);  // Calling Initialize for the second time?
	
	// Create the DB tables, if they don't exist:
	m_DB.exec("CREATE TABLE IF NOT EXISTS Rank (RankID INTEGER PRIMARY KEY, Name, MsgPrefix, MsgSuffix, MsgNameColorCode)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS PlayerRank (PlayerUUID, PlayerName, RankID INTEGER)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS PermGroup (PermGroupID INTEGER PRIMARY KEY, Name)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS RankPermGroup (RankID INTEGER, PermGroupID INTEGER)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS PermissionItem (PermGroupID INTEGER, Permission)");
	m_DB.exec("CREATE TABLE IF NOT EXISTS DefaultRank (RankID INTEGER)");
	
	m_IsInitialized = true;
	
	a_MojangAPI.SetRankManager(this);

	// Check if tables empty, migrate from ini files then
	if (AreDBTablesEmpty())
	{
		LOGINFO("There are no ranks, migrating old-style INI files to new DB ranks...");
		LOGINFO("(This might take a while)");
		cRankManagerIniMigrator Migrator(*this, a_MojangAPI);
		if (Migrator.Migrate())
		{
			LOGINFO("Ranks migrated.");
			// The default rank has been set by the migrator
			return;
		}

		// Migration failed. Add some defaults
		LOGINFO("Rank migration failed, creating default ranks...");
		CreateDefaults();
		LOGINFO("Default ranks created.");
	}
	
	// Load the default rank:
	try
	{
		SQLite::Statement stmt(m_DB,
			"SELECT Rank.Name FROM Rank "
			"LEFT JOIN DefaultRank ON Rank.RankID = DefaultRank.RankID"
		);
		if (stmt.executeStep())
		{
			m_DefaultRank = stmt.getColumn(0).getText();
		}
	}
	catch (const SQLite::Exception & ex)
	{
		LOGWARNING("%s: Cannot load default rank: %s", __FUNCTION__, ex.what());
		return;
	}

	// If the default rank cannot be loaded, use the first rank:
	if (m_DefaultRank.empty())
	{
		SetDefaultRank(GetAllRanks()[0]);
	}
}





AString cRankManager::GetPlayerRankName(const AString & a_PlayerUUID)
{
	ASSERT(m_IsInitialized);
	cCSLock Lock(m_CS);
	
	try
	{
		SQLite::Statement stmt(m_DB, "SELECT Rank.Name FROM Rank LEFT JOIN PlayerRank ON Rank.RankID = PlayerRank.RankID WHERE PlayerRank.PlayerUUID = ?");
		stmt.bind(1, a_PlayerUUID);
		// executeStep returns false on no data
		if (!stmt.executeStep())
		{
			// No data returned from the DB
			return AString();
		}
		return stmt.getColumn(0).getText();
	}
	catch (const SQLite::Exception & ex)
	{
		LOGWARNING("%s: Cannot get player rank name: %s", __FUNCTION__, ex.what());
	}
	return AString();
}



{'fraction_non_alphanumeric': 0.1388389086769728, 'fraction_numerical': 0.0009547240480299637, 'mean_word_length': 3.4529103989535646, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '468470', 'n_tokens_mistral': 10494, 'n_tokens_neox': 9260, 'n_words': 2425}