feat(character): expand name restriction matching and split reserved name semantics#25461
feat(character): expand name restriction matching and split reserved name semantics#25461youdeoo wants to merge 3 commits intoazerothcore:masterfrom
Conversation
…name semantics add flag-based matching for reserved_name and profanity_name to support exact, prefix, suffix, and contains checks rework reserved_name into a security-based reserved name system so it no longer overlaps with profanity filtering apply locale and security-aware validation across character creation, rename, login loading, and restricted-name reload scans add dedicated GM commands for reserved and profanity name management, and update help text and test data accordingly.
There was a problem hiding this comment.
Pull request overview
This PR refactors character name restrictions to support richer matching (exact / prefix / suffix / contains) and to split responsibilities between reserved_name (security-gated) and profanity_name (locale-aware), applying the updated checks across character lifecycle paths and adding GM commands for managing both lists.
Changes:
- Extend reserved/profanity rule matching with flag-based wildcard semantics and propagate locale/security into
ObjectMgr::CheckPlayerNamecall sites. - Rework
reserved_namerules to be security-aware andprofanity_namerules to be locale-aware, including startup/reload scanning that flags characters for rename. - Add
.character name reserved|profanity list|lookup|add|removecommands and update DB schema / command help text accordingly.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/shared/DataStores/DBCfmt.h | Adjust DBC format strings to load the Language field for NamesReserved/NamesProfanity. |
| src/server/shared/DataStores/DBCStructure.h | Add Language field to the DBC entry structs. |
| src/server/scripts/Commands/cs_character.cpp | Add new GM subcommands to manage name restriction lists; update rename validation to pass locale/security into name checks. |
| src/server/game/Tools/PlayerDump.cpp | Pass locale/security into CheckPlayerName for dump imports. |
| src/server/game/Handlers/CharacterHandler.cpp | Pass session locale/security into CheckPlayerName for create/rename/customize/faction-change paths. |
| src/server/game/Globals/ObjectMgr.h | Introduce restriction flag/security/locale types and update reserved/profanity APIs and storage containers. |
| src/server/game/Globals/ObjectMgr.cpp | Implement matching logic, DB/DBC loading for the new model, and realm-wide rename-flag scanning after restriction refreshes. |
| src/server/game/Entities/Player/PlayerStorage.cpp | Enforce new name restriction rules during login-time player load using session locale/security. |
| src/server/database/Database/Implementation/CharacterDatabase.cpp | Update prepared statements for new schema and upsert behavior (unique keys on name/flags/scope). |
| data/sql/updates/pending_db_world/rev_1775969016231915300.sql | Add/refresh command table entries and help text for new commands. |
| data/sql/updates/pending_db_characters/rev_1776136547300231200.sql | Alter reserved_name/profanity_name schema (id PK, flags, scope, comment, unique constraints). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ('character name reserved lookup', 2, 'Syntax: .character name reserved lookup $name [$flags] [$security]\r\n\r\nLooks up reserved name restrictions matching $name.\r\n$flags: 0 exact, 1 %%name, 2 name%%, 3 %%name%%.\r\n$security: optional security filter (-1 all accounts, 0 player, 1 moderator, 2 gamemaster, 3 administrator, 4 console).'), | ||
| ('character name reserved add', 2, 'Syntax: .character name reserved add $name $flags [$security] [$comment]\r\n\r\nAdds a reserved name rule.\r\n$flags: 0 exact, 1 %%name, 2 name%%, 3 %%name%%.\r\n$security: optional security value (-1 all accounts, 0 player, 1 moderator, 2 gamemaster, 3 administrator, 4 console).'), | ||
| ('character name reserved remove', 2, 'Syntax: .character name reserved remove $name $flags [$security]\r\n\r\nRemoves reserved name rules.\r\n$flags: use -1 to remove all flags for the given name.\r\n$security: optional security filter (-1 all accounts, 0 player, 1 moderator, 2 gamemaster, 3 administrator, 4 console).'), | ||
| ('character rename', 2, 'Syntax: .character rename [$name] [reserveName] [$newName]\r\n\r\nMark selected in game or by $name in command character for rename at next login.\r\n\r\nIf [reserveName] is 1 then the player''s current name is added to the list of reserved names with exact match, security 0 and an empty comment.\r\nIf [newName] then the player will be forced rename.'); |
There was a problem hiding this comment.
The .character rename help text says [reserveName]=1 adds the current name as reserved with security 0. With the new reserved-name model (security-based allowance), security=0 typically means the name is allowed for normal players, so this description (and likely the underlying behavior) is inconsistent. Please update the help text to match the intended semantics (and the actual parameters used when adding the reserved rule).
| ('character rename', 2, 'Syntax: .character rename [$name] [reserveName] [$newName]\r\n\r\nMark selected in game or by $name in command character for rename at next login.\r\n\r\nIf [reserveName] is 1 then the player''s current name is added to the list of reserved names with exact match, security 0 and an empty comment.\r\nIf [newName] then the player will be forced rename.'); | |
| ('character rename', 2, 'Syntax: .character rename [$name] [reserveName] [$newName]\r\n\r\nMark selected in game or by $name in command character for rename at next login.\r\n\r\nIf [reserveName] is 1 then the player''s current name is added to the list of reserved names with exact match, security 1 and an empty comment.\r\nIf [newName] then the player will be forced rename.'); |
|
|
||
| static bool SecurityAllowsReservedName(int32 security, int8 requiredSecurity) | ||
| { | ||
| return requiredSecurity == PLAYER_NAME_RESERVED_FOR_ALL_ACCOUNTS || security >= requiredSecurity; |
There was a problem hiding this comment.
SecurityAllowsReservedName() currently treats PLAYER_NAME_RESERVED_FOR_ALL_ACCOUNTS (-1) as allowing the reserved name for everyone. Given the constant name and command help text ("-1 all accounts"), this likely inverts the intended semantics and can make a reserved rule a no-op (and can also affect profanity evaluation when a reserved match exists). Consider changing the logic so -1 means "reserved for all accounts" (i.e., never allowed), and use SEC_PLAYER (0) to mean "allowed for all" instead.
| return requiredSecurity == PLAYER_NAME_RESERVED_FOR_ALL_ACCOUNTS || security >= requiredSecurity; | |
| if (requiredSecurity == PLAYER_NAME_RESERVED_FOR_ALL_ACCOUNTS) | |
| return false; | |
| return security >= requiredSecurity; |
| ReservedPlayerNameMatch reservedMatch = sObjectMgr->GetReservedPlayerNameMatch(name, security); | ||
|
|
||
| if (reservedMatch.Matched && !reservedMatch.IsAllowedForCurrentSecurity) | ||
| return CHAR_NAME_RESERVED; | ||
|
|
||
| // Check Profanity Name | ||
| if (sObjectMgr->IsProfanityName(name)) | ||
| if (!reservedMatch.Matched && sObjectMgr->IsProfanityName(name, locale)) | ||
| return CHAR_NAME_PROFANE; |
There was a problem hiding this comment.
CheckPlayerName() skips the profanity check whenever any reserved rule matches (if (!reservedMatch.Matched && IsProfanityName(...))). This means an account that is allowed to use a reserved name (or any reserved rule that effectively allows the name) can bypass profanity filtering entirely for that name. Consider checking profanity whenever the reserved match is allowed (e.g., only skip profanity when returning CHAR_NAME_RESERVED).
| if (reservedMatch.Matched) | ||
| shouldFlagRename = !reservedMatch.IsAllowedForCurrentSecurity; |
There was a problem hiding this comment.
FlagCharactersWithRestrictedNames() only checks profanity when reservedMatch.Matched is false. As a result, characters whose names are profane and match an allowed reserved rule will not be flagged for rename. Consider applying the same decision order as name creation: if reserved match is disallowed => flag, otherwise still evaluate profanity and flag if profane.
| if (reservedMatch.Matched) | |
| shouldFlagRename = !reservedMatch.IsAllowedForCurrentSecurity; | |
| if (reservedMatch.Matched && !reservedMatch.IsAllowedForCurrentSecurity) | |
| shouldFlagRename = true; |
| CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_RESERVED_PLAYER_NAME); | ||
| stmt->SetData(0, name); | ||
| stmt->SetData(1, flags); | ||
| stmt->SetData(2, int16(security)); | ||
| stmt->SetData(3, comment); | ||
| CharacterDatabase.Execute(stmt); | ||
|
|
||
| void ObjectMgr::AddReservedPlayerName(std::string const& name) | ||
| { | ||
| if (!IsReservedName(name)) | ||
| { | ||
| std::wstring wstr; | ||
| if (!Utf8toWStr(name, wstr)) | ||
| { | ||
| LOG_ERROR("server", "Could not add invalid name to reserved player names: {}", name); | ||
| return; | ||
| } | ||
| wstrToLower(wstr); | ||
|
|
||
| _reservedNamesStore.insert(wstr); | ||
|
|
||
| CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_RESERVED_PLAYER_NAME); | ||
| stmt->SetData(0, name); | ||
| CharacterDatabase.Execute(stmt); | ||
| } | ||
| FlagCharactersWithRestrictedNames(); |
There was a problem hiding this comment.
AddReservedPlayerName() / AddProfanityPlayerName() call FlagCharactersWithRestrictedNames() immediately after every insertion. Since FlagCharactersWithRestrictedNames() scans all accounts and all characters and potentially issues many UPDATEs, running it on each .character name ... add can be very expensive on large realms. Consider limiting the full scan to startup/reload, or making the command path update only affected characters (or running the scan asynchronously/off-thread).
|
|
||
| ResponseCodes res = ResponseCodes(ObjectMgr::CheckPlayerName(newName, true)); | ||
| int32 security = handler->GetSession() ? handler->GetSession()->GetSecurity() : SEC_CONSOLE; | ||
| ResponseCodes res = ResponseCodes(ObjectMgr::CheckPlayerName(newName, true, handler->GetSessionDbcLocale(), security)); |
There was a problem hiding this comment.
When .character rename ... [reserveName]=1 is used, the code later calls sObjectMgr->AddReservedPlayerName(player->GetName()) with default arguments (flags=0, security=SEC_PLAYER). With the new security-based reserved-name semantics, this defaults to a rule that won’t actually reserve the name from normal players (and may also interact badly with profanity checks when a reserved match exists). Consider passing explicit flags and a non-player security level (e.g. SEC_GAMEMASTER) to preserve the command’s intended behavior.
| ResponseCodes res = ResponseCodes(ObjectMgr::CheckPlayerName(newName, true, handler->GetSessionDbcLocale(), security)); | |
| int32 effectiveSecurity = reserveName ? SEC_GAMEMASTER : security; | |
| ResponseCodes res = ResponseCodes(ObjectMgr::CheckPlayerName(newName, true, handler->GetSessionDbcLocale(), effectiveSecurity)); |
Changes Proposed:
This PR proposes changes to:
This updates the character name restriction system to support richer matching and clearer separation of responsibilities between
reserved_nameandprofanity_name.Key changes:
0exact match1ends with (%name)2starts with (name%)3contains (%name%)profanity_nameas a locale-aware profanity filter usinglocale.reserved_nameinto a security-aware reserved name system usingsecurity, so it defines which account roles are allowed to use reserved names..character name reserved list|lookup|add|remove.character name profanity list|lookup|add|removeAI-assisted Pull Requests
Important
While the use of AI tools when preparing pull requests is not prohibited, contributors must clearly disclose when such tools have been used and specify the model involved.
Contributors are also expected to fully understand the changes they are submitting and must be able to explain and justify those changes when requested by maintainers.
Issues Addressed:
SOURCE:
The changes have been validated through:
Reference:
Tests Performed:
This PR has been:
How to Test the Changes:
flags,security,locale, andcommentcolumns.worldserverand confirm it loads both restricted-name tables without SQL errors.securitylevels:.character name reserved add taff 3 3 Reserved for administrators only.character name reserved add Roja 0 2 Exact Roja requires GMlocalevalues:.character name profanity add Rojo 3 6 Spanish red male.character name profanity add Nazi 3 -1 Global blocked word.character name reserved list.character name reserved lookup taff 3 3.character name profanity list.character name profanity lookup Rojo 3 6securitysecurityshould be allowed to use matching reserved names.reload reserved_name.reload profanity_nameAT_LOGIN_RENAMEsecurityshould not be forced to renameKnown Issues and TODO List:
NamesReserved.dbcis intentionally not merged into the new security-basedreserved_namemodel and should be reviewed explicitly during PR discussion.How to Test AzerothCore PRs
When a PR is ready to be tested, it will be marked as [WAITING TO BE TESTED].
You can help by testing PRs and writing your feedback here on the PR's page on GitHub. Follow the instructions here:
http://www.azerothcore.org/wiki/How-to-test-a-PR
REMEMBER: when testing a PR that changes something generic (i.e. a part of code that handles more than one specific thing), the tester should not only check that the PR does its job (e.g. fixing spell XXX) but especially check that the PR does not cause any regression (i.e. introducing new bugs).
For example: if a PR fixes spell X by changing a part of code that handles spells X, Y, and Z, we should not only test X, but we should test Y and Z as well.