diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/server/game/Accounts/AccountMgr.cpp | 14 | ||||
-rw-r--r-- | src/server/game/Accounts/AccountMgr.h | 1 | ||||
-rw-r--r-- | src/server/game/Chat/Chat.cpp | 14 | ||||
-rw-r--r-- | src/server/game/Chat/Chat.h | 22 | ||||
-rw-r--r-- | src/server/game/Chat/Commands/Level3.cpp | 2 | ||||
-rw-r--r-- | src/server/game/Entities/Player/Player.cpp | 310 | ||||
-rw-r--r-- | src/server/game/Entities/Player/Player.h | 13 | ||||
-rw-r--r-- | src/server/game/Miscellaneous/Language.h | 13 | ||||
-rw-r--r-- | src/server/game/Tools/PlayerDump.cpp | 17 | ||||
-rw-r--r-- | src/server/game/World/World.cpp | 16 | ||||
-rw-r--r-- | src/server/game/World/World.h | 6 | ||||
-rw-r--r-- | src/server/shared/Utilities/Util.h | 9 | ||||
-rw-r--r-- | src/server/worldserver/CommandLine/CliRunnable.cpp | 318 | ||||
-rw-r--r-- | src/server/worldserver/worldserver.conf.dist | 27 |
14 files changed, 641 insertions, 141 deletions
diff --git a/src/server/game/Accounts/AccountMgr.cpp b/src/server/game/Accounts/AccountMgr.cpp index 54f80114131..54c4f4ffc35 100644 --- a/src/server/game/Accounts/AccountMgr.cpp +++ b/src/server/game/Accounts/AccountMgr.cpp @@ -61,6 +61,7 @@ AccountOpResult AccountMgr::DeleteAccount(uint32 accid) if (!result) return AOR_NAME_NOT_EXIST; // account doesn't exist + // existed characters list result = CharacterDatabase.PQuery("SELECT guid FROM characters WHERE account='%d'",accid); if (result) { @@ -214,6 +215,19 @@ bool AccountMgr::CheckPassword(uint32 accid, std::string passwd) return false; } +uint32 AccountMgr::GetCharactersCount(uint32 acc_id) +{ + uint32 charcount = 0; + // check character count + QueryResult_AutoPtr result = CharacterDatabase.PQuery("SELECT COUNT(guid) FROM characters WHERE account = '%d'", acc_id); + if (result) + { + Field *fields=result->Fetch(); + charcount = fields[0].GetUInt32(); + } + return charcount; +} + bool AccountMgr::normalizeString(std::string& utf8str) { wchar_t wstr_buf[MAX_ACCOUNT_STR+1]; diff --git a/src/server/game/Accounts/AccountMgr.h b/src/server/game/Accounts/AccountMgr.h index a0e181cf4a0..f53d29032b7 100644 --- a/src/server/game/Accounts/AccountMgr.h +++ b/src/server/game/Accounts/AccountMgr.h @@ -53,6 +53,7 @@ class AccountMgr uint32 GetSecurity(uint32 acc_id); uint32 GetSecurity(uint32 acc_id, int32 realm_id); bool GetName(uint32 acc_id, std::string &name); + uint32 GetCharactersCount(uint32 acc_id); std::string CalculateShaPassHash(std::string& name, std::string& password); static bool normalizeString(std::string& utf8str); diff --git a/src/server/game/Chat/Chat.cpp b/src/server/game/Chat/Chat.cpp index 42363124518..5efd4b4ef6a 100644 --- a/src/server/game/Chat/Chat.cpp +++ b/src/server/game/Chat/Chat.cpp @@ -119,10 +119,20 @@ ChatCommand * ChatHandler::getCommandTable() { NULL, 0, false, NULL, "", NULL } }; + static ChatCommand characterDeletedCommandTable[] = + { + { "delete", SEC_CONSOLE, true, &ChatHandler::HandleCharacterDeletedDeleteCommand, "", NULL }, + { "list", SEC_ADMINISTRATOR, true, &ChatHandler::HandleCharacterDeletedListCommand, "", NULL }, + { "restore", SEC_ADMINISTRATOR, true, &ChatHandler::HandleCharacterDeletedRestoreCommand, "", NULL }, + { "old", SEC_CONSOLE, true, &ChatHandler::HandleCharacterDeletedOldCommand, "", NULL }, + { NULL, 0, false, NULL, "", NULL } + }; + static ChatCommand characterCommandTable[] = { { "customize", SEC_GAMEMASTER, true, &ChatHandler::HandleCharacterCustomizeCommand, "", NULL }, - { "delete", SEC_CONSOLE, true, &ChatHandler::HandleCharacterDeleteCommand, "", NULL }, + { "deleted", SEC_GAMEMASTER, true, NULL, "", characterDeletedCommandTable}, + { "erase", SEC_CONSOLE, true, &ChatHandler::HandleCharacterEraseCommand, "", NULL }, { "level", SEC_ADMINISTRATOR, true, &ChatHandler::HandleCharacterLevelCommand, "", NULL }, { "rename", SEC_GAMEMASTER, true, &ChatHandler::HandleCharacterRenameCommand, "", NULL }, { "reputation", SEC_GAMEMASTER, true, &ChatHandler::HandleCharacterReputationCommand, "", NULL }, @@ -667,7 +677,7 @@ ChatCommand * ChatHandler::getCommandTable() { "modify", SEC_MODERATOR, false, NULL, "", modifyCommandTable }, { "debug", SEC_MODERATOR, true, NULL, "", debugCommandTable }, { "tele", SEC_MODERATOR, true, NULL, "", teleCommandTable }, - { "character", SEC_GAMEMASTER, false, NULL, "", characterCommandTable}, + { "character", SEC_GAMEMASTER, true, NULL, "", characterCommandTable}, { "event", SEC_GAMEMASTER, false, NULL, "", eventCommandTable }, { "gobject", SEC_GAMEMASTER, false, NULL, "", gobjectCommandTable }, { "honor", SEC_GAMEMASTER, false, NULL, "", honorCommandTable }, diff --git a/src/server/game/Chat/Chat.h b/src/server/game/Chat/Chat.h index 2a653983aa0..1a649c687d1 100644 --- a/src/server/game/Chat/Chat.h +++ b/src/server/game/Chat/Chat.h @@ -133,7 +133,11 @@ class ChatHandler bool HandleCastTargetCommand(const char *args); bool HandleCharacterCustomizeCommand(const char * args); - bool HandleCharacterDeleteCommand(const char* args); + bool HandleCharacterDeletedDeleteCommand(const char* args); + bool HandleCharacterDeletedListCommand(const char* args); + bool HandleCharacterDeletedRestoreCommand(const char* args); + bool HandleCharacterDeletedOldCommand(const char* args); + bool HandleCharacterEraseCommand(const char* args); bool HandleCharacterLevelCommand(const char* args); bool HandleCharacterRenameCommand(const char * args); bool HandleCharacterReputationCommand(const char* args); @@ -620,6 +624,22 @@ class ChatHandler void HandleCharacterLevel(Player* player, uint64 player_guid, uint32 oldlevel, uint32 newlevel); void HandleLearnSkillRecipesHelper(Player* player,uint32 skill_id); + // Stores informations about a deleted character + struct DeletedInfo + { + uint32 lowguid; ///< the low GUID from the character + std::string name; ///< the character name + uint32 accountId; ///< the account id + std::string accountName; ///< the account name + time_t deleteDate; ///< the date at which the character has been deleted + }; + + typedef std::list<DeletedInfo> DeletedInfoList; + bool GetDeletedCharacterInfoList(DeletedInfoList& foundList, std::string searchString = ""); + std::string GenerateDeletedCharacterGUIDsWhereStr(DeletedInfoList::const_iterator& itr, DeletedInfoList::const_iterator const& itr_end); + void HandleCharacterDeletedListHelper(DeletedInfoList const& foundList); + void HandleCharacterDeletedRestoreHelper(DeletedInfo const& delInfo); + void SetSentErrorMessage(bool val){ sentErrorMessage = val;}; private: WorldSession * m_session; // != NULL for chat command call and NULL for CLI command diff --git a/src/server/game/Chat/Commands/Level3.cpp b/src/server/game/Chat/Commands/Level3.cpp index 40c3aae5705..049df5a342f 100644 --- a/src/server/game/Chat/Commands/Level3.cpp +++ b/src/server/game/Chat/Commands/Level3.cpp @@ -6522,7 +6522,7 @@ bool ChatHandler::HandlePDumpWriteCommand(const char *args) uint32 guid; // character name can't start from number - if (isNumeric(p2[0])) + if (isNumeric(p2)) guid = atoi(p2); else { diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 94ca4bd20f6..4caa328839b 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -4343,22 +4343,41 @@ TrainerSpellState Player::GetTrainerSpellState(TrainerSpell const* trainer_spell return TRAINER_SPELL_GREEN; } -void Player::DeleteFromDB(uint64 playerguid, uint32 accountId, bool updateRealmChars) +/** + * Deletes a character from the database + * + * The way, how the characters will be deleted is decided based on the config option. + * + * @see Player::DeleteOldCharacters + * + * @param playerguid the low-GUID from the player which should be deleted + * @param accountId the account id from the player + * @param updateRealmChars when this flag is set, the amount of characters on that realm will be updated in the realmlist + * @param deleteFinally if this flag is set, the config option will be ignored and the character will be permanently removed from the database + */ +void Player::DeleteFromDB(uint64 playerguid, uint32 accountId, bool updateRealmChars, bool deleteFinally) { + // for not existed account avoid update realm + if (accountId == 0) + updateRealmChars = false; + + uint32 charDelete_method = sWorld.getConfig(CONFIG_CHARDELETE_METHOD); + uint32 charDelete_minLvl = sWorld.getConfig(CONFIG_CHARDELETE_MIN_LEVEL); + + // if we want to finally delete the character or the character does not meet the level requirement, + // we set it to mode CHAR_DELETE_REMOVE + if (deleteFinally || Player::GetLevelFromDB(playerguid) < charDelete_minLvl) + charDelete_method = CHAR_DELETE_REMOVE; + uint32 guid = GUID_LOPART(playerguid); // convert corpse to bones if exist (to prevent exiting Corpse in World without DB entry) // bones will be deleted by corpse/bones deleting thread shortly sObjectAccessor.ConvertCorpseForPlayer(playerguid); - // remove from guild - uint32 guildId = GetGuildIdFromDB(playerguid); - if (guildId != 0) - { - Guild* guild = objmgr.GetGuildById(guildId); - if (guild) + if (uint32 guildId = GetGuildIdFromDB(playerguid)) + if (Guild* guild = objmgr.GetGuildById(guildId)) guild->DelMember(guid); - } // remove from arena teams LeaveAllArenaTeams(playerguid); @@ -4372,139 +4391,194 @@ void Player::DeleteFromDB(uint64 playerguid, uint32 accountId, bool updateRealmC RemoveFromGroup(group, playerguid); } - // remove signs from petitions (also remove petitions if owner); + // Remove signs from petitions (also remove petitions if owner); RemovePetitionsAndSigns(playerguid, 10); - // return back all mails with COD and Item 0 1 2 3 4 5 6 7 - QueryResult_AutoPtr resultMail = CharacterDatabase.PQuery("SELECT id,messageType,mailTemplateId,sender,subject,body,money,has_items FROM mail WHERE receiver='%u' AND has_items<>0 AND cod<>0", guid); - if (resultMail) + switch (charDelete_method) { - do + // Completely remove from the database + case CHAR_DELETE_REMOVE: { - Field *fields = resultMail->Fetch(); - - uint32 mail_id = fields[0].GetUInt32(); - uint16 mailType = fields[1].GetUInt16(); - uint16 mailTemplateId= fields[2].GetUInt16(); - uint32 sender = fields[3].GetUInt32(); - std::string subject = fields[4].GetCppString(); - std::string body = fields[5].GetCppString(); - uint32 money = fields[6].GetUInt32(); - bool has_items = fields[7].GetBool(); - - //we can return mail now - //so firstly delete the old one - CharacterDatabase.PExecute("DELETE FROM mail WHERE id = '%u'", mail_id); - - // mail not from player - if (mailType != MAIL_NORMAL) - { - if (has_items) - CharacterDatabase.PExecute("DELETE FROM mail_items WHERE mail_id = '%u'", mail_id); - continue; - } - - MailDraft draft(subject, body); - if (mailTemplateId) - draft = MailDraft(mailTemplateId,false); // itesm already included - - if (has_items) + // Return back all mails with COD and Item 0 1 2 3 4 5 6 7 + QueryResult_AutoPtr resultMail = CharacterDatabase.PQuery("SELECT id,messageType,mailTemplateId,sender,subject,body,money,has_items FROM mail WHERE receiver='%u' AND has_items<>0 AND cod<>0", guid); + if (resultMail) { - // data needs to be at first place for Item::LoadFromDB - QueryResult_AutoPtr resultItems = CharacterDatabase.PQuery("SELECT data,text,item_guid,item_template FROM mail_items JOIN item_instance ON item_guid = guid WHERE mail_id='%u'", mail_id); - if (resultItems) + do { - do + Field *fields = resultMail->Fetch(); + + uint32 mail_id = fields[0].GetUInt32(); + uint16 mailType = fields[1].GetUInt16(); + uint16 mailTemplateId= fields[2].GetUInt16(); + uint32 sender = fields[3].GetUInt32(); + std::string subject = fields[4].GetCppString(); + std::string body = fields[5].GetCppString(); + uint32 money = fields[6].GetUInt32(); + bool has_items = fields[7].GetBool(); + + // We can return mail now + // So firstly delete the old one + CharacterDatabase.PExecute("DELETE FROM mail WHERE id = '%u'", mail_id); + + // Mail is not from player + if (mailType != MAIL_NORMAL) { - Field *fields2 = resultItems->Fetch(); + if (has_items) + CharacterDatabase.PExecute("DELETE FROM mail_items WHERE mail_id = '%u'", mail_id); + continue; + } - uint32 item_guidlow = fields2[2].GetUInt32(); - uint32 item_template = fields2[3].GetUInt32(); + MailDraft draft(subject, body); + if (mailTemplateId) + draft = MailDraft(mailTemplateId,false); // items are already included - ItemPrototype const* itemProto = objmgr.GetItemPrototype(item_template); - if (!itemProto) + if (has_items) + { + // Data needs to be at first place for Item::LoadFromDB + QueryResult_AutoPtr resultItems = CharacterDatabase.PQuery("SELECT data,text,item_guid,item_template FROM mail_items JOIN item_instance ON item_guid = guid WHERE mail_id='%u'", mail_id); + if (resultItems) { - CharacterDatabase.PExecute("DELETE FROM item_instance WHERE guid = '%u'", item_guidlow); - continue; - } + do + { + Field *fields2 = resultItems->Fetch(); - Item *pItem = NewItemOrBag(itemProto); - if (!pItem->LoadFromDB(item_guidlow, MAKE_NEW_GUID(guid, 0, HIGHGUID_PLAYER),resultItems)) - { - pItem->FSetState(ITEM_REMOVED); - pItem->SaveToDB(); // it also deletes item object ! - continue; - } + uint32 item_guidlow = fields2[2].GetUInt32(); + uint32 item_template = fields2[3].GetUInt32(); + + ItemPrototype const* itemProto = objmgr.GetItemPrototype(item_template); + if (!itemProto) + { + CharacterDatabase.PExecute("DELETE FROM item_instance WHERE guid = '%u'", item_guidlow); + continue; + } + + Item *pItem = NewItemOrBag(itemProto); + if (!pItem->LoadFromDB(item_guidlow, MAKE_NEW_GUID(guid, 0, HIGHGUID_PLAYER),resultItems)) + { + pItem->FSetState(ITEM_REMOVED); + pItem->SaveToDB(); // it also deletes item object! + continue; + } - draft.AddItem(pItem); + draft.AddItem(pItem); + } + while (resultItems->NextRow()); + } } - while (resultItems->NextRow()); + + CharacterDatabase.PExecute("DELETE FROM mail_items WHERE mail_id = '%u'", mail_id); + + uint32 pl_account = objmgr.GetPlayerAccountIdByGUID(MAKE_NEW_GUID(guid, 0, HIGHGUID_PLAYER)); + + draft.AddMoney(money).SendReturnToSender(pl_account, guid, sender); } + while (resultMail->NextRow()); } - CharacterDatabase.PExecute("DELETE FROM mail_items WHERE mail_id = '%u'", mail_id); - - uint32 pl_account = objmgr.GetPlayerAccountIdByGUID(MAKE_NEW_GUID(guid, 0, HIGHGUID_PLAYER)); + // Unsummon and delete for pets in world is not required: player deleted from CLI or character list with not loaded pet. + // Get guids of character's pets, will deleted in transaction + QueryResult_AutoPtr resultPets = CharacterDatabase.PQuery("SELECT id FROM character_pet WHERE owner = '%u'",guid); - draft.AddMoney(money).SendReturnToSender(pl_account, guid, sender); + // NOW we can finally clear other DB data related to character + CharacterDatabase.BeginTransaction(); + if (resultPets) + { + do + { + Field *fields3 = resultPets->Fetch(); + uint32 petguidlow = fields3[0].GetUInt32(); + Pet::DeleteFromDB(petguidlow); + } while (resultPets->NextRow()); + } + + CharacterDatabase.PExecute("DELETE FROM characters WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_account_data WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_declinedname WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_action WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_aura WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_gifts WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_homebind WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_instance WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM group_instance WHERE leaderGuid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM groups WHERE leaderGuid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM group_member WHERE leaderGuid = '%u' OR memberGuid = '%u'",guid,guid); + CharacterDatabase.PExecute("DELETE FROM groups WHERE leaderGuid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_inventory WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_queststatus WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_reputation WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_spell WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_spell_cooldown WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM gm_tickets WHERE playerGuid = '%u'", guid); + CharacterDatabase.PExecute("DELETE FROM item_instance WHERE owner_guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_social WHERE guid = '%u' OR friend='%u'",guid,guid); + CharacterDatabase.PExecute("DELETE FROM mail WHERE receiver = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM mail_items WHERE receiver = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_pet WHERE owner = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_pet_declinedname WHERE owner = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_achievement WHERE guid = '%u' " // NOTE: These achievements have flags & 256 in DBC. + "AND achievement NOT BETWEEN '456' AND '467' " // Realm First Level 80 + "AND achievement NOT BETWEEN '1400' AND '1427' " // Realm First Raid Achievements + "AND achievement NOT IN(1463, 3117, 3259) ", guid); // Realm First Northen Vanguard + Raid Achievements + CharacterDatabase.PExecute("DELETE FROM character_achievement_progress WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_equipmentsets WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM guild_eventlog WHERE PlayerGuid1 = '%u' OR PlayerGuid2 = '%u'",guid, guid); + CharacterDatabase.PExecute("DELETE FROM guild_bank_eventlog WHERE PlayerGuid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_battleground_data WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_glyphs WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_queststatus_daily WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_talent WHERE guid = '%u'",guid); + CharacterDatabase.PExecute("DELETE FROM character_skills WHERE guid = '%u'",guid); + + CharacterDatabase.CommitTransaction(); + break; } - while (resultMail->NextRow()); + // The character gets unlinked from the account, the name gets freed up and appears as deleted ingame + case CHAR_DELETE_UNLINK: + CharacterDatabase.PExecute("UPDATE characters SET deleteInfos_Name=name, deleteInfos_Account=account, deleteDate='" UI64FMTD "', name='', account=0 WHERE guid=%u", uint64(time(NULL)), guid); + break; + default: + sLog.outError("Player::DeleteFromDB: Unsupported delete method: %u.", charDelete_method); } - // unsummon and delete for pets in world is not required: player deleted from CLI or character list with not loaded pet. - // Get guids of character's pets, will deleted in transaction - QueryResult_AutoPtr resultPets = CharacterDatabase.PQuery("SELECT id FROM character_pet WHERE owner = '%u'",guid); + if (updateRealmChars) + sWorld.UpdateRealmCharCount(accountId); +} - // NOW we can finally clear other DB data related to character - CharacterDatabase.BeginTransaction(); - if (resultPets) - { - do - { - Field *fields3 = resultPets->Fetch(); - uint32 petguidlow = fields3[0].GetUInt32(); - Pet::DeleteFromDB(petguidlow); - } while (resultPets->NextRow()); - } - - CharacterDatabase.PExecute("DELETE FROM characters WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_account_data WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_declinedname WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_action WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_aura WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_gifts WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_homebind WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_instance WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_inventory WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_queststatus WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_reputation WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_spell WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_spell_cooldown WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM gm_tickets WHERE playerGuid = '%u'", guid); - CharacterDatabase.PExecute("DELETE FROM item_instance WHERE owner_guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_social WHERE guid = '%u' OR friend='%u'",guid,guid); - CharacterDatabase.PExecute("DELETE FROM mail WHERE receiver = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM mail_items WHERE receiver = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_pet WHERE owner = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_pet_declinedname WHERE owner = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_achievement WHERE guid = '%u' " // NOTE: These achievements have flags & 256 in DBC. - "AND achievement NOT BETWEEN '456' AND '467' " // Realm First Level 80 - "AND achievement NOT BETWEEN '1400' AND '1427' " // Realm First Raid Achievements - "AND achievement NOT IN(1463, 3117, 3259) ", guid); // Realm First Northen Vanguard + Raid Achievements - CharacterDatabase.PExecute("DELETE FROM character_achievement_progress WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_equipmentsets WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM guild_eventlog WHERE PlayerGuid1 = '%u' OR PlayerGuid2 = '%u'",guid, guid); - CharacterDatabase.PExecute("DELETE FROM guild_bank_eventlog WHERE PlayerGuid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_battleground_data WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_glyphs WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_queststatus_daily WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_talent WHERE guid = '%u'",guid); - CharacterDatabase.PExecute("DELETE FROM character_skills WHERE guid = '%u'",guid); +/** + * Characters which were kept back in the database after being deleted and are now too old (see config option "CharDelete.KeepDays"), will be completely deleted. + * + * @see Player::DeleteFromDB + */ +void Player::DeleteOldCharacters() +{ + uint32 keepDays = sWorld.getConfig(CONFIG_CHARDELETE_KEEP_DAYS); + if (!keepDays) + return; - CharacterDatabase.CommitTransaction(); + Player::DeleteOldCharacters(keepDays); +} - //LoginDatabase.PExecute("UPDATE realmcharacters SET numchars = numchars - 1 WHERE acctid = %d AND realmid = %d", accountId, realmID); - if (updateRealmChars) sWorld.UpdateRealmCharCount(accountId); +/** + * Characters which were kept back in the database after being deleted and are older than the specified amount of days, will be completely deleted. + * + * @see Player::DeleteFromDB + * + * @param keepDays overrite the config option by another amount of days + */ +void Player::DeleteOldCharacters(uint32 keepDays) +{ + sLog.outString("Player::DeleteOldChars: Deleting all characters which have been deleted %u days before...", keepDays); + + QueryResult_AutoPtr resultChars = CharacterDatabase.PQuery("SELECT guid, deleteInfos_Account FROM characters WHERE deleteDate IS NOT NULL AND deleteDate < '" UI64FMTD "'", uint64(time(NULL) - time_t(keepDays * DAY))); + if (resultChars) + { + sLog.outString("Player::DeleteOldChars: Found %u character(s) to delete",resultChars->GetRowCount()); + do + { + Field *charFields = resultChars->Fetch(); + Player::DeleteFromDB(charFields[0].GetUInt64(), charFields[1].GetUInt32(), true, true); + } while(resultChars->NextRow()); + } } void Player::SetMovement(PlayerMovementType pType) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 97c1031899d..d568d226042 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -839,6 +839,13 @@ struct AccessRequirement std::string heroicQuestFailedText; }; +enum CharDeleteMethod +{ + CHAR_DELETE_REMOVE = 0, // Completely remove from the database + CHAR_DELETE_UNLINK = 1 // The character gets unlinked from the account, + // the name gets freed up and appears as deleted ingame +}; + class PlayerTaxi { public: @@ -1418,6 +1425,10 @@ class Player : public Unit, public GridObject<Player> static void Customize(uint64 guid, uint8 gender, uint8 skin, uint8 face, uint8 hairStyle, uint8 hairColor, uint8 facialHair); static void SavePositionInDB(uint32 mapid, float x,float y,float z,float o,uint32 zone,uint64 guid); + static void DeleteFromDB(uint64 playerguid, uint32 accountId, bool updateRealmChars = true, bool deleteFinally = false); + static void DeleteOldCharacters(); + static void DeleteOldCharacters(uint32 keepDays); + bool m_mailsLoaded; bool m_mailsUpdated; @@ -1836,8 +1847,6 @@ class Player : public Unit, public GridObject<Player> void SendTeleportPacket(Position &oldPos); void SendTeleportAckPacket(); - static void DeleteFromDB(uint64 playerguid, uint32 accountId, bool updateRealmChars = true); - Corpse *GetCorpse() const; void SpawnCorpseBones(); void CreateCorpse(); diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 435c6549f56..abf92a14b45 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -748,7 +748,18 @@ enum TrinityStrings LANG_ACCOUNT_LIST_LINE = 1013, LANG_ACCOUNT_LIST_EMPTY = 1014, LANG_ACCOUNT_LIST_BAR_HEADER = 1015, - // Room for more level 4 1016-1099 not used + LANG_CHARACTER_DELETED_LIST_HEADER = 1016, + LANG_CHARACTER_DELETED_LIST_LINE_CONSOLE = 1017, + LANG_CHARACTER_DELETED_LIST_BAR = 1018, + LANG_CHARACTER_DELETED_LIST_EMPTY = 1019, + LANG_CHARACTER_DELETED_RESTORE = 1020, + LANG_CHARACTER_DELETED_DELETE = 1021, + LANG_CHARACTER_DELETED_ERR_RENAME = 1022, + LANG_CHARACTER_DELETED_SKIP_ACCOUNT = 1023, + LANG_CHARACTER_DELETED_SKIP_FULL = 1024, + LANG_CHARACTER_DELETED_SKIP_NAME = 1025, + LANG_CHARACTER_DELETED_LIST_LINE_CHAT = 1026, + // Room for more level 4 1027-1099 not used // Level 3 (continue) LANG_ACCOUNT_SETADDON = 1100, diff --git a/src/server/game/Tools/PlayerDump.cpp b/src/server/game/Tools/PlayerDump.cpp index 964e7f9b3b1..79b2ae0b1ff 100644 --- a/src/server/game/Tools/PlayerDump.cpp +++ b/src/server/game/Tools/PlayerDump.cpp @@ -24,6 +24,7 @@ #include "SQLStorage.h" #include "UpdateFields.h" #include "ObjectMgr.h" +#include "AccountMgr.h" #define DUMP_TABLE_COUNT 26 struct DumpTable @@ -359,19 +360,9 @@ DumpReturn PlayerDumpWriter::WriteDump(const std::string& file, uint32 guid) DumpReturn PlayerDumpReader::LoadDump(const std::string& file, uint32 account, std::string name, uint32 guid) { - // check character count - { - QueryResult_AutoPtr result = CharacterDatabase.PQuery("SELECT COUNT(guid) FROM characters WHERE account = '%d'", account); - uint8 charcount = 0; - if (result) - { - Field *fields=result->Fetch(); - charcount = fields[0].GetUInt8(); - - if (charcount >= 10) - return DUMP_TOO_MANY_CHARS; - } - } + uint32 charcount = accmgr.GetCharactersCount(account); + if (charcount >= 10) + return DUMP_TOO_MANY_CHARS; FILE *fin = fopen(file.c_str(), "r"); if (!fin) diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index ff7b8a472c2..0c5505fa16a 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1172,6 +1172,11 @@ void World::LoadConfigSettings(bool reload) m_visibility_notify_periodInInstances = sConfig.GetIntDefault("Visibility.Notify.Period.InInstances", DEFAULT_VISIBILITY_NOTIFY_PERIOD); m_visibility_notify_periodInBGArenas = sConfig.GetIntDefault("Visibility.Notify.Period.InBGArenas", DEFAULT_VISIBILITY_NOTIFY_PERIOD); + ///- Load the CharDelete related config options + m_configs[CONFIG_CHARDELETE_METHOD] = sConfig.GetIntDefault("CharDelete.Method", 0); + m_configs[CONFIG_CHARDELETE_MIN_LEVEL] = sConfig.GetIntDefault("CharDelete.MinLevel", 0); + m_configs[CONFIG_CHARDELETE_KEEP_DAYS] = sConfig.GetIntDefault("CharDelete.KeepDays", 30); + ///- Read the "Data" directory from the config file std::string dataPath = sConfig.GetStringDefault("DataDir","./"); if (dataPath.at(dataPath.length()-1) != '/' && dataPath.at(dataPath.length()-1) != '\\') @@ -1639,6 +1644,7 @@ void World::SetInitialWorldSettings() m_timers[WUPDATE_CLEANDB].SetInterval(m_configs[CONFIG_LOGDB_CLEARINTERVAL]*MINUTE*IN_MILLISECONDS); // clean logs table every 14 days by default m_timers[WUPDATE_AUTOBROADCAST].SetInterval(abtimer); + m_timers[WUPDATE_DELETECHARS].SetInterval(DAY*IN_MILLISECONDS); // check for chars to delete every day //to set mailtimer to return mails every day between 4 and 5 am //mailtimer is increased when updating auctions @@ -1660,6 +1666,9 @@ void World::SetInitialWorldSettings() uint32 nextGameEvent = gameeventmgr.Initialize(); m_timers[WUPDATE_EVENTS].SetInterval(nextGameEvent); //depend on next event + // Delete all characters which have been deleted X days before + Player::DeleteOldCharacters(); + sLog.outString("Starting Arena Season..."); gameeventmgr.StartArenaSeason(); @@ -1958,6 +1967,13 @@ void World::Update(uint32 diff) sOutdoorPvPMgr.Update(diff); RecordTimeDiff("UpdateOutdoorPvPMgr"); + ///- Delete all characters which have been deleted X days before + if (m_timers[WUPDATE_DELETECHARS].Passed()) + { + m_timers[WUPDATE_DELETECHARS].Reset(); + Player::DeleteOldCharacters(); + } + sLFGMgr.Update(diff); RecordTimeDiff("UpdateLFGMgr"); diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index f3a92108f60..88c845213a2 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -84,7 +84,8 @@ enum WorldTimers WUPDATE_CLEANDB = 7, WUPDATE_AUTOBROADCAST = 8, WUPDATE_MAILBOXQUEUE = 9, - WUPDATE_COUNT = 10 + WUPDATE_DELETECHARS = 10, + WUPDATE_COUNT = 11 }; /// Configuration elements @@ -281,6 +282,9 @@ enum WorldConfigs CONFIG_BG_XP_FOR_KILL, CONFIG_RANDOM_BG_RESET_HOUR, CONFIG_VMAP_INDOOR_CHECK, + CONFIG_CHARDELETE_KEEP_DAYS, + CONFIG_CHARDELETE_METHOD, + CONFIG_CHARDELETE_MIN_LEVEL, CONFIG_VALUE_COUNT }; diff --git a/src/server/shared/Utilities/Util.h b/src/server/shared/Utilities/Util.h index 4f997725d25..68886bb0d42 100644 --- a/src/server/shared/Utilities/Util.h +++ b/src/server/shared/Utilities/Util.h @@ -182,6 +182,15 @@ inline bool isNumeric(char c) return (c >= '0' && c <='9'); } +inline bool isNumeric(char const* str) +{ + for (char const* c = str; *c; ++c) + if (!isNumeric(*c)) + return false; + + return true; +} + inline bool isNumericOrSpace(wchar_t wchar) { return isNumeric(wchar) || wchar == L' '; diff --git a/src/server/worldserver/CommandLine/CliRunnable.cpp b/src/server/worldserver/CommandLine/CliRunnable.cpp index c72e454700c..23a023972c9 100644 --- a/src/server/worldserver/CommandLine/CliRunnable.cpp +++ b/src/server/worldserver/CommandLine/CliRunnable.cpp @@ -163,8 +163,322 @@ bool ChatHandler::HandleAccountDeleteCommand(const char* args) return true; } -bool ChatHandler::HandleCharacterDeleteCommand(const char* args) +/** + * Collects all GUIDs (and related info) from deleted characters which are still in the database. + * + * @param foundList a reference to an std::list which will be filled with info data + * @param searchString the search string which either contains a player GUID or a part fo the character-name + * @return returns false if there was a problem while selecting the characters (e.g. player name not normalizeable) + */ +bool ChatHandler::GetDeletedCharacterInfoList(DeletedInfoList& foundList, std::string searchString) +{ + QueryResult_AutoPtr resultChar; + if (!searchString.empty()) + { + // search by GUID + if (isNumeric(searchString.c_str())) + resultChar = CharacterDatabase.PQuery("SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate FROM characters WHERE deleteDate IS NOT NULL AND guid = %u", uint64(atoi(searchString.c_str()))); + // search by name + else + { + if(!normalizePlayerName(searchString)) + return false; + + resultChar = CharacterDatabase.PQuery("SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate FROM characters WHERE deleteDate IS NOT NULL AND deleteInfos_Name " _LIKE_ " " _CONCAT3_("'%%'", "'%s'", "'%%'"), searchString.c_str()); + } + } + else + resultChar = CharacterDatabase.Query("SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate FROM characters WHERE deleteDate IS NOT NULL"); + + if (resultChar) + { + do + { + Field* fields = resultChar->Fetch(); + + DeletedInfo info; + + info.lowguid = fields[0].GetUInt32(); + info.name = fields[1].GetCppString(); + info.accountId = fields[2].GetUInt32(); + + // account name will be empty for not existed account + accmgr.GetName(info.accountId, info.accountName); + + info.deleteDate = time_t(fields[3].GetUInt64()); + + foundList.push_back(info); + } while (resultChar->NextRow()); + } + + return true; +} + +/** + * Generate WHERE guids list by deleted info in way preventing return too long where list for existed query string length limit. + * + * @param itr a reference to an deleted info list iterator, it updated in function for possible next function call if list to long + * @param itr_end a reference to an deleted info list iterator end() + * @return returns generated where list string in form: 'guid IN (gui1, guid2, ...)' + */ +std::string ChatHandler::GenerateDeletedCharacterGUIDsWhereStr(DeletedInfoList::const_iterator& itr, DeletedInfoList::const_iterator const& itr_end) { + std::ostringstream wherestr; + wherestr << "guid IN ('"; + for(; itr != itr_end; ++itr) + { + wherestr << itr->lowguid; + + if (wherestr.str().size() > MAX_QUERY_LEN - 50) // near to max query + { + ++itr; + break; + } + + DeletedInfoList::const_iterator itr2 = itr; + if(++itr2 != itr_end) + wherestr << "','"; + } + wherestr << "')"; + return wherestr.str(); +} + +/** + * Shows all deleted characters which matches the given search string, expected non empty list + * + * @see ChatHandler::HandleCharacterDeletedListCommand + * @see ChatHandler::HandleCharacterDeletedRestoreCommand + * @see ChatHandler::HandleCharacterDeletedDeleteCommand + * @see ChatHandler::DeletedInfoList + * + * @param foundList contains a list with all found deleted characters + */ +void ChatHandler::HandleCharacterDeletedListHelper(DeletedInfoList const& foundList) +{ + if (!m_session) + { + SendSysMessage(LANG_CHARACTER_DELETED_LIST_BAR); + SendSysMessage(LANG_CHARACTER_DELETED_LIST_HEADER); + SendSysMessage(LANG_CHARACTER_DELETED_LIST_BAR); + } + + for (DeletedInfoList::const_iterator itr = foundList.begin(); itr != foundList.end(); ++itr) + { + std::string dateStr = TimeToTimestampStr(itr->deleteDate); + + if (!m_session) + PSendSysMessage(LANG_CHARACTER_DELETED_LIST_LINE_CONSOLE, + itr->lowguid, itr->name.c_str(), itr->accountName.empty() ? "<Not existed>" : itr->accountName.c_str(), + itr->accountId, dateStr.c_str()); + else + PSendSysMessage(LANG_CHARACTER_DELETED_LIST_LINE_CHAT, + itr->lowguid, itr->name.c_str(), itr->accountName.empty() ? "<Not existed>" : itr->accountName.c_str(), + itr->accountId, dateStr.c_str()); + } + + if (!m_session) + SendSysMessage(LANG_CHARACTER_DELETED_LIST_BAR); +} + +/** + * Handles the '.character deleted list' command, which shows all deleted characters which matches the given search string + * + * @see ChatHandler::HandleCharacterDeletedListHelper + * @see ChatHandler::HandleCharacterDeletedRestoreCommand + * @see ChatHandler::HandleCharacterDeletedDeleteCommand + * @see ChatHandler::DeletedInfoList + * + * @param args the search string which either contains a player GUID or a part fo the character-name + */ +bool ChatHandler::HandleCharacterDeletedListCommand(const char* args) +{ + DeletedInfoList foundList; + if (!GetDeletedCharacterInfoList(foundList, args)) + return false; + + // if no characters have been found, output a warning + if (foundList.empty()) + { + SendSysMessage(LANG_CHARACTER_DELETED_LIST_EMPTY); + return false; + } + + HandleCharacterDeletedListHelper(foundList); + return true; +} + +/** + * Restore a previously deleted character + * + * @see ChatHandler::HandleCharacterDeletedListHelper + * @see ChatHandler::HandleCharacterDeletedRestoreCommand + * @see ChatHandler::HandleCharacterDeletedDeleteCommand + * @see ChatHandler::DeletedInfoList + * + * @param delInfo the informations about the character which will be restored + */ +void ChatHandler::HandleCharacterDeletedRestoreHelper(DeletedInfo const& delInfo) +{ + if (delInfo.accountName.empty()) // account not exist + { + PSendSysMessage(LANG_CHARACTER_DELETED_SKIP_ACCOUNT, delInfo.name.c_str(), delInfo.lowguid, delInfo.accountId); + return; + } + + // check character count + uint32 charcount = accmgr.GetCharactersCount(delInfo.accountId); + if (charcount >= 10) + { + PSendSysMessage(LANG_CHARACTER_DELETED_SKIP_FULL, delInfo.name.c_str(), delInfo.lowguid, delInfo.accountId); + return; + } + + if (objmgr.GetPlayerGUIDByName(delInfo.name)) + { + PSendSysMessage(LANG_CHARACTER_DELETED_SKIP_NAME, delInfo.name.c_str(), delInfo.lowguid, delInfo.accountId); + return; + } + + CharacterDatabase.PExecute("UPDATE characters SET name='%s', account='%u', deleteDate=NULL, deleteInfos_Name=NULL, deleteInfos_Account=NULL WHERE deleteDate IS NOT NULL AND guid = %u", + delInfo.name.c_str(), delInfo.accountId, delInfo.lowguid); +} + +/** + * Handles the '.character deleted restore' command, which restores all deleted characters which matches the given search string + * + * The command automatically calls '.character deleted list' command with the search string to show all restored characters. + * + * @see ChatHandler::HandleCharacterDeletedRestoreHelper + * @see ChatHandler::HandleCharacterDeletedListCommand + * @see ChatHandler::HandleCharacterDeletedDeleteCommand + * + * @param args the search string which either contains a player GUID or a part of the character-name + */ +bool ChatHandler::HandleCharacterDeletedRestoreCommand(const char* args) +{ + // It is required to submit at least one argument + if (!*args) + return false; + + std::string searchString; + std::string newCharName; + uint32 newAccount = 0; + + // GCC by some strange reason fail build code without temporary variable + std::istringstream params(args); + params >> searchString >> newCharName >> newAccount; + + DeletedInfoList foundList; + if (!GetDeletedCharacterInfoList(foundList, searchString)) + return false; + + if (foundList.empty()) + { + SendSysMessage(LANG_CHARACTER_DELETED_LIST_EMPTY); + return false; + } + + SendSysMessage(LANG_CHARACTER_DELETED_RESTORE); + HandleCharacterDeletedListHelper(foundList); + + if (newCharName.empty()) + { + // Drop not existed account cases + for (DeletedInfoList::iterator itr = foundList.begin(); itr != foundList.end(); ++itr) + HandleCharacterDeletedRestoreHelper(*itr); + } + else if (foundList.size() == 1 && normalizePlayerName(newCharName)) + { + DeletedInfo delInfo = foundList.front(); + + // update name + delInfo.name = newCharName; + + // if new account provided update deleted info + if (newAccount && newAccount != delInfo.accountId) + { + delInfo.accountId = newAccount; + accmgr.GetName(newAccount, delInfo.accountName); + } + + HandleCharacterDeletedRestoreHelper(delInfo); + } + else + SendSysMessage(LANG_CHARACTER_DELETED_ERR_RENAME); + + return true; +} + +/** + * Handles the '.character deleted delete' command, which completely deletes all deleted characters which matches the given search string + * + * @see Player::GetDeletedCharacterGUIDs + * @see Player::DeleteFromDB + * @see ChatHandler::HandleCharacterDeletedListCommand + * @see ChatHandler::HandleCharacterDeletedRestoreCommand + * + * @param args the search string which either contains a player GUID or a part fo the character-name + */ +bool ChatHandler::HandleCharacterDeletedDeleteCommand(const char* args) +{ + // It is required to submit at least one argument + if (!*args) + return false; + + DeletedInfoList foundList; + if (!GetDeletedCharacterInfoList(foundList, args)) + return false; + + if (foundList.empty()) + { + SendSysMessage(LANG_CHARACTER_DELETED_LIST_EMPTY); + return false; + } + + SendSysMessage(LANG_CHARACTER_DELETED_DELETE); + HandleCharacterDeletedListHelper(foundList); + + // Call the appropriate function to delete them (current account for deleted characters is 0) + for(DeletedInfoList::const_iterator itr = foundList.begin(); itr != foundList.end(); ++itr) + Player::DeleteFromDB(itr->lowguid, 0, false, true); + + return true; +} + +/** + * Handles the '.character deleted old' command, which completely deletes all deleted characters deleted with some days ago + * + * @see Player::DeleteOldCharacters + * @see Player::DeleteFromDB + * @see ChatHandler::HandleCharacterDeletedDeleteCommand + * @see ChatHandler::HandleCharacterDeletedListCommand + * @see ChatHandler::HandleCharacterDeletedRestoreCommand + * + * @param args the search string which either contains a player GUID or a part fo the character-name + */ +bool ChatHandler::HandleCharacterDeletedOldCommand(const char* args) +{ + int32 keepDays = sWorld.getConfig(CONFIG_CHARDELETE_KEEP_DAYS); + + char* px = strtok((char*)args, " "); + if (px) + { + if (!isNumeric(px)) + return false; + + keepDays = atoi(px); + if (keepDays < 0) + return false; + } + // config option value 0 -> disabled and can't be used + else if (keepDays <= 0) + return false; + + Player::DeleteOldCharacters((uint32)keepDays); + return true; +} + +bool ChatHandler::HandleCharacterEraseCommand(const char* args){ if(!*args) return false; @@ -202,7 +516,7 @@ bool ChatHandler::HandleCharacterDeleteCommand(const char* args) std::string account_name; accmgr.GetName (account_id,account_name); - Player::DeleteFromDB(character_guid, account_id, true); + Player::DeleteFromDB(character_guid, account_id, true, true); PSendSysMessage(LANG_CHARACTER_DELETED,character_name.c_str(),GUID_LOPART(character_guid),account_name.c_str(), account_id); return true; } diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 0f93bef4c06..2751212fae4 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -2087,6 +2087,33 @@ Ra.Port = 3443 Ra.MinLevel = 3 Ra.Secure = 1 +################################################################################################################### +# CharDelete.Method +# Character deletion behavior +# Default: 0 - Completely remove the character from the database +# 1 - Unlinking, the character gets unlinked from the account, +# the name gets freed up and appears as deleted ingame +# +# CharDelete.MinLevel +# Character gets deleted by CharDelete.Method=0 when the character +# hasn't the specified level yet. +# Default: 0 - For all characters the specified mode will be used +# 1+ - Only for players which have reached the specified level +# will be deleted by the specified mode. +# the rest will be deleted by CharDelete.Method=0 +# +# CharDelete.KeepDays +# Define the amount of days for which the characters are kept in the database before +# they will be removed +# Default: 30 +# 0 - Don't delete any characters, they stay in the database forever. +# +################################################################################################################### + +CharDelete.Method = 0 +CharDelete.MinLevel = 0 +CharDelete.KeepDays = 30 + ############################################################################### # CUSTOM SERVER OPTIONS # |