боты русифицированы

This commit is contained in:
2026-07-30 19:06:57 +04:00
parent 6f075133f7
commit 85adbe19da
26 changed files with 11211 additions and 28 deletions
@@ -17,6 +17,7 @@
#include "QuestDef.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include "WorldSession.h"
#include <cstdlib>
#include <string>
@@ -31,6 +32,8 @@ namespace MoonWell::PlayerGuide
return;
if (!player || !player->GetSession() || player->IsBeingTeleported())
return;
if (player->GetSession()->IsBot())
return;
Payload p = sPlayerGuideMgr->BuildFor(player);
std::string json = sPlayerGuideMgr->SerializeJson(p);
@@ -25,6 +25,29 @@ namespace MoonWell::PlayerGuide
// The mgr clamps its configured chunk size into a safe range.
constexpr std::size_t HARD_LIMIT = 250;
std::size_t Utf8ChunkSize(std::string_view body, std::size_t offset,
std::size_t budget)
{
std::size_t take = std::min(budget, body.size() - offset);
if (offset + take == body.size())
return take;
// A chunk must not end between a UTF-8 leading byte and one of
// its continuation bytes. Chat packet validation rejects such
// an individually invalid string before the addon can reassemble
// the complete JSON payload.
while (take > 0 &&
(static_cast<unsigned char>(body[offset + take]) & 0xC0) == 0x80)
{
--take;
}
// The configured budget is clamped to at least 64 bytes, so a
// valid UTF-8 code point always fits. Keep a defensive fallback
// for malformed input to guarantee forward progress.
return take > 0 ? take : std::min(budget, body.size() - offset);
}
void SendOneChunk(Player* player, std::string_view command,
uint32 seq, uint32 total, std::string_view body)
{
@@ -71,16 +94,20 @@ namespace MoonWell::PlayerGuide
return;
}
// Multi-chunk: split blindly by byte budget. JSON tolerates
// concatenation on the client.
uint32 total = static_cast<uint32>(
(body.size() + budget - 1) / budget);
uint32 seq = 1;
for (std::size_t off = 0; off < body.size(); off += budget, ++seq)
// Calculate UTF-8-safe slices first because backing up from a
// continuation byte can increase the number of chunks.
std::vector<std::string_view> chunks;
for (std::size_t off = 0; off < body.size();)
{
std::size_t take = std::min(budget, body.size() - off);
SendOneChunk(player, command, seq, total,
std::string_view(body.data() + off, take));
std::size_t take = Utf8ChunkSize(body, off, budget);
chunks.emplace_back(body.data() + off, take);
off += take;
}
uint32 total = static_cast<uint32>(chunks.size());
for (uint32 i = 0; i < total; ++i)
{
SendOneChunk(player, command, i + 1, total, chunks[i]);
}
}
@@ -84,6 +84,10 @@
# Enable or disable Playerbots module
AiPlayerbot.Enabled = 1
# Use the MoonWell Cyrillic name and guild-name pools.
# Existing RNDbot characters can be migrated with localize-playerbots-ru.sh.
AiPlayerbot.UseRussianNames = 0
# Enable randombot system
AiPlayerbot.RandomBotAutologin = 1
@@ -12,6 +12,7 @@
#include "Event.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "Util.h"
static const std::unordered_set<std::string> noReplyMsgs = {
"join",
@@ -52,6 +53,18 @@ static const std::unordered_set<std::string> noReplyMsgParts = {
"+", "-", "@", "follow target", "focus heal", "cast ", "accept [", "e [", "destroy [", "go zone"};
static const std::unordered_set<std::string> noReplyMsgStarts = {"e ", "accept ", "cast ", "destroy "};
static std::string Utf8Lowercase(std::string const& value)
{
std::wstring wide;
if (!Utf8toWStr(value, wide))
return value;
wstrToLower(wide);
std::string lowered;
WStrToUtf8(wide, lowered);
return lowered;
}
SayAction::SayAction(PlayerbotAI* botAI) : Action(botAI, "say"), Qualified() {}
bool SayAction::Execute(Event /*event*/)
@@ -578,6 +591,40 @@ std::string ChatReplyAction::GenerateReplyMessage(Player* bot, std::string& inco
std::string respondsText = "";
// The legacy free-chat parser below is English-specific and constructs
// several responses directly in English. For ruRU, classify the small
// static reply set locally and let PlayerbotTextMgr select text_loc8.
if (PlayerbotTextMgr::instance().GetLocalePriority() == LOCALE_ruRU)
{
std::string const message = Utf8Lowercase(incomingMessage);
std::string const botName = Utf8Lowercase(bot->GetName());
if (Player* player = ObjectAccessor::FindPlayer(ObjectGuid(HighGuid::Player, guid1));
player && player->isGMChat())
{
replyType = REPLY_ADMIN_ABUSE;
}
else if (message.find("привет") != std::string::npos || message.find("здравств") != std::string::npos ||
message.find("добрый день") != std::string::npos || message.find("добрый вечер") != std::string::npos)
{
replyType = REPLY_HELLO;
}
else if (message.find("нуб") != std::string::npos || message.find("дурак") != std::string::npos ||
message.find("идиот") != std::string::npos || message.find("заткнись") != std::string::npos)
{
replyType = REPLY_GRUDGE;
}
else if (message.find(botName) != std::string::npos)
{
replyType = REPLY_NAME;
}
respondsText = PlayerbotTextMgr::instance().GetBotText(replyType, name);
if (respondsText.size() > 255)
respondsText.resize(255);
return respondsText;
}
// Chat Logic
int32 verb_pos = -1;
int32 verb_type = -1;
@@ -175,16 +175,17 @@ Player* RandomPlayerbotFactory::CreateRandomBot(WorldSession* session, uint8 cls
std::string const RandomPlayerbotFactory::CreateRandomBotName(NameRaceAndGender raceAndGender)
{
std::string botName = "";
char const* namesTable = sPlayerbotAIConfig.useRussianNames ? "playerbots_names_ru" : "playerbots_names";
int tries = 3;
while (--tries)
{
QueryResult result = CharacterDatabase.Query(
"SELECT n.name "
"FROM playerbots_names n "
"FROM {} n "
"LEFT OUTER JOIN characters c ON c.name = n.name "
"WHERE c.guid IS NULL and n.gender = '{}' "
"ORDER BY RAND() LIMIT 1",
static_cast<uint8>(raceAndGender));
namesTable, static_cast<uint8>(raceAndGender));
if (!result)
{
break;
@@ -668,7 +669,8 @@ void RandomPlayerbotFactory::CreateRandomBots()
{
nameCached = true;
LOG_INFO("playerbots", "Creating cache for names per gender and race...");
QueryResult result = CharacterDatabase.Query("SELECT name, gender FROM playerbots_names");
char const* namesTable = sPlayerbotAIConfig.useRussianNames ? "playerbots_names_ru" : "playerbots_names";
QueryResult result = CharacterDatabase.Query("SELECT name, gender FROM {}", namesTable);
if (!result)
{
LOG_ERROR("playerbots", "No more unused names left");
@@ -753,8 +755,10 @@ void RandomPlayerbotFactory::CreateRandomBots()
std::string const RandomPlayerbotFactory::CreateRandomGuildName()
{
std::string guildName = "";
char const* guildNamesTable =
sPlayerbotAIConfig.useRussianNames ? "playerbots_guild_names_ru" : "playerbots_guild_names";
QueryResult result = CharacterDatabase.Query("SELECT MAX(name_id) FROM playerbots_guild_names");
QueryResult result = CharacterDatabase.Query("SELECT MAX(name_id) FROM {}", guildNamesTable);
if (!result)
{
LOG_ERROR("playerbots", "No more names left for random guilds");
@@ -766,9 +770,9 @@ std::string const RandomPlayerbotFactory::CreateRandomGuildName()
uint32 id = urand(0, maxId);
result = CharacterDatabase.Query(
"SELECT n.name FROM playerbots_guild_names n "
"SELECT n.name FROM {} n "
"LEFT OUTER JOIN guild e ON e.name = n.name WHERE e.guildid IS NULL AND n.name_id >= {} LIMIT 1",
id);
guildNamesTable, id);
if (!result)
{
LOG_ERROR("playerbots", "No more names left for random guilds");
@@ -1652,7 +1652,7 @@ void PlayerbotMgr::OnPlayerLogin(Player* player)
// For bot texts (DB-driven), prefer the database locale with a safe fallback.
LocaleConstant usedLocale = databaseLocale;
if (usedLocale >= MAX_LOCALES)
if (usedLocale >= TOTAL_LOCALES)
usedLocale = LOCALE_enUS; // fallback
// set locale priority for bot texts
@@ -161,13 +161,15 @@ void PlayerbotGuildMgr::ResetGuildCache()
void PlayerbotGuildMgr::LoadGuildNames()
{
LOG_INFO("playerbots", "Loading guild names from playerbots_guild_names...");
char const* guildNamesTable =
sPlayerbotAIConfig.useRussianNames ? "playerbots_guild_names_ru" : "playerbots_guild_names";
LOG_INFO("playerbots", "Loading guild names from {}...", guildNamesTable);
QueryResult result = CharacterDatabase.Query("SELECT name_id, name FROM playerbots_guild_names");
QueryResult result = CharacterDatabase.Query("SELECT name_id, name FROM {}", guildNamesTable);
if (!result)
{
LOG_ERROR("playerbots", "No entries found in playerbots_guild_names. List is empty.");
LOG_ERROR("playerbots", "No entries found in {}. List is empty.", guildNamesTable);
return;
}
@@ -184,7 +186,7 @@ void PlayerbotGuildMgr::LoadGuildNames()
std::mt19937 g(rd());
std::shuffle(_shuffled_guild_keys.begin(), _shuffled_guild_keys.end(), g);
LOG_INFO("playerbots", "Loaded {} guild entries from playerbots_guild_names table.", _guildNames.size());
LOG_INFO("playerbots", "Loaded {} guild entries from {} table.", _guildNames.size(), guildNamesTable);
}
void PlayerbotGuildMgr::ValidateGuildCache()
@@ -38,7 +38,7 @@ void PlayerbotTextMgr::LoadBotTexts()
text[0] = fields[1].Get<std::string>();
uint8 sayType = fields[2].Get<uint8>();
uint8 replyType = fields[3].Get<uint8>();
for (uint8 i = 1; i < MAX_LOCALES; ++i)
for (uint8 i = 1; i < TOTAL_LOCALES; ++i)
{
text[i] = fields[i + 3].Get<std::string>();
}
@@ -192,9 +192,10 @@ bool PlayerbotTextMgr::GetBotText(std::string name, std::string& text, std::map<
void PlayerbotTextMgr::AddLocalePriority(uint32 locale)
{
if (locale >= MAX_LOCALES)
if (locale >= TOTAL_LOCALES)
{
LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds MAX_LOCALES ({})", locale, MAX_LOCALES - 1);
LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds TOTAL_LOCALES ({})", locale,
TOTAL_LOCALES - 1);
return;
}
@@ -212,7 +213,7 @@ uint32 PlayerbotTextMgr::GetLocalePriority()
}
uint32 topLocale = 0;
for (uint8 i = 0; i < MAX_LOCALES; ++i)
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
{
if (botTextLocalePriority[i] > botTextLocalePriority[topLocale])
topLocale = i;
@@ -223,7 +224,7 @@ uint32 PlayerbotTextMgr::GetLocalePriority()
void PlayerbotTextMgr::ResetLocalePriority()
{
for (uint8 i = 0; i < MAX_LOCALES; ++i)
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
{
botTextLocalePriority[i] = 0;
}
@@ -87,7 +87,7 @@ public:
private:
PlayerbotTextMgr()
{
for (uint8 i = 0; i < MAX_LOCALES; ++i)
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
{
botTextLocalePriority[i] = 0;
}
@@ -102,7 +102,7 @@ private:
std::map<std::string, std::vector<BotTextEntry>> botTexts;
std::map<std::string, uint32> botTextChance;
uint32 botTextLocalePriority[MAX_LOCALES];
uint32 botTextLocalePriority[TOTAL_LOCALES];
};
#endif
@@ -67,6 +67,8 @@ bool PlayerbotAIConfig::Initialize()
return false;
}
useRussianNames = sConfigMgr->GetOption<bool>("AiPlayerbot.UseRussianNames", false);
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
@@ -79,6 +79,7 @@ public:
bool IsInPvpProhibitedArea(uint32 id);
bool enabled;
bool useRussianNames;
bool disabledWithoutRealPlayer;
bool EnableICCBuffs;
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;