Compare commits

..

12 Commits

Author SHA1 Message Date
sindoring 65f3535382 Передавать production realm в bnetserver 2026-09-14 09:41:53 +04:00
sindoring 7657dd4731 Использовать ruRU для DB2 по умолчанию 2026-09-13 15:34:35 +04:00
sindoring 25d255ddad Merge remote-tracking branch 'upstream/sylvaniacore' 2026-09-13 12:46:44 +04:00
sindoring 2b6e2f12fb fix 2026-09-13 12:45:55 +04:00
BlaMacfly 0c87f040f6 Difficulte : traiter CMSG_TOGGLE_DIFFICULTY, le mode heroique des vieux raids
Signalement Discord : impossible de passer les Terres de feu en heroique, le
client repondait qu on ne remplissait pas les conditions. Aucun rapport avec un
deblocage par kill.

Les raids d avant Warlords n utilisent pas CMSG_SET_RAID_DIFFICULTY pour ce
bouton : le client envoie CMSG_TOGGLE_DIFFICULTY, sans preciser de difficulte,
et attend la bascule normal <-> heroique a taille de raid constante. L opcode
etait declare STATUS_UNHANDLED et jete a la reception, d ou le refus cote
client. C est aussi pourquoi le 10 <-> 25 fonctionnait : ce bouton passe par
l autre opcode.

Handler ecrit avec les memes garde-fous que le changement de difficulte
ordinaire. Reste a faire : la bascule depuis l interieur de l instance, le vrai
comportement de Cataclysm -- IsDynamicDifficultyMap existe dans les DB2 mais
n est utilise nulle part.
2026-09-12 23:02:02 +02:00
BlaMacfly 08426fa6d4 Terres de feu et Zul Aman : trois blocages signales en jeu
Nalorakk restait plante au-dessus de son escalier apres la deuxieme vague. Le
trajet entre deux paliers grimpe une structure VMAP (le terrain, lui, reste a
z=12,5 sur toute la rampe) ; quand le calcul de chemin cale, le boss s arrete
en route et, comme il attend d etre arrive pour enchainer, l evenement ne
repart plus. Chaque deplacement est desormais borne a vingt secondes, au-dela
le boss est pose d office sur l etape.

Alysrazor finissait toujours dans la lave : la ramener dans JustDied arrivait
trop tard, la chute etait engagee, et MoveFall vise le terrain (z=48) en
traversant la plateforme, qui n est qu une structure VMAP a 55,5. Elle est
maintenant posee au centre de l arene AVANT le coup fatal, via DamageTaken, et
plus aucune chute n est declenchee.

Rhyolith restait bloque en premiere forme a un point de vie. Son UpdateAI
sortait sur UpdateVictim avant d avoir teste la bascule en phase 2 -- or les
joueurs frappent les pieds et non le colosse, qui se retrouve sans cible. Le
passage de phase est teste en premier.
2026-09-12 19:47:24 +02:00
sindoring 0a277174bc fix 2026-09-12 21:13:31 +04:00
sindoring 6956edf6cd перевод французских модулей 2026-09-12 19:18:12 +04:00
sindoring edb998de02 fix 2026-09-12 18:29:57 +04:00
sindoring 4d760528cc перевод на русский 2026-09-12 18:02:23 +04:00
sindoring a0464d3388 fix 2026-09-12 17:03:50 +04:00
sindoring c2f72294d8 поиск строк на французском для перевода 2026-09-12 16:30:07 +04:00
31 changed files with 9882 additions and 149 deletions
+1
View File
@@ -5,6 +5,7 @@ EXTERNAL_ADDRESS=127.0.0.1
REALM_NAME=Sylvania Solo
REALM_ADDRESS=127.0.0.1
REALM_PORT=8086
DBC_LOCALE=8
BUILD_JOBS=2
BUILD_TYPE=Release
IMAGE_TAG=local
+2 -1
View File
@@ -5,7 +5,8 @@ DB_PASSWORD=GENERATE_A_DIFFERENT_LONG_RANDOM_DB_PASSWORD
REALM_NAME=FelWell
REALM_ADDRESS=legion.moon-well.online
REALM_PORT=8085
REALM_PORT=8086
DBC_LOCALE=8
EXTERNAL_ADDRESS=legion.moon-well.online
BUILD_JOBS=8
+8
View File
@@ -20,6 +20,14 @@ target_compile_definitions(trinity-compile-option-interface
INTERFACE
-DHAVE_SSE2
-D__SSE2__)
# Player-facing module messages are stored in the repository as UTF-8.
# Keep their byte representation deterministic even when the build host uses
# a different system locale.
target_compile_options(trinity-compile-option-interface
INTERFACE
-finput-charset=UTF-8
-fexec-charset=UTF-8)
message(STATUS "GCC: SFMT enabled, SSE2 flags forced")
if( WITH_WARNINGS )
+5
View File
@@ -20,6 +20,7 @@ x-core: &core
REALM_NAME: ${REALM_NAME:-Sylvania Solo}
REALM_ADDRESS: ${REALM_ADDRESS:-127.0.0.1}
REALM_PORT: ${REALM_PORT:-8086}
DBC_LOCALE: ${DBC_LOCALE:-8}
volumes:
- ./data:/opt/sylvania/data:ro
- ./sql:/src/sql:ro
@@ -53,6 +54,10 @@ services:
DB_PORT: 3306
DB_USER: ${DB_USER:-sylvania}
DB_PASSWORD: ${DB_PASSWORD:-sylvania}
REALM_NAME: ${REALM_NAME:-Sylvania Solo}
REALM_ADDRESS: ${REALM_ADDRESS:-127.0.0.1}
REALM_PORT: ${REALM_PORT:-8086}
DBC_LOCALE: ${DBC_LOCALE:-8}
EXTERNAL_ADDRESS: ${EXTERNAL_ADDRESS:-127.0.0.1}
ports:
- "1119:1119"
+7 -7
View File
@@ -5,7 +5,7 @@ db_user="${DB_USER:-sylvania}"
db_password="${DB_PASSWORD:-sylvania}"
# Identifiers are deliberately fixed; only the application login is configurable.
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" <<SQL
mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" <<SQL
CREATE DATABASE IF NOT EXISTS auth CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE IF NOT EXISTS characters CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE IF NOT EXISTS world CHARACTER SET utf8 COLLATE utf8_general_ci;
@@ -20,21 +20,21 @@ GRANT ALL PRIVILEGES ON shop.* TO '${db_user}'@'%';
FLUSH PRIVILEGES;
SQL
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" auth < /sylvania/base/auth_database.sql
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" characters < /sylvania/base/characters_database.sql
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" shop < /sylvania/base/shop_database.sql
mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" auth < /sylvania/base/auth_database.sql
mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" characters < /sylvania/base/characters_database.sql
mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" shop < /sylvania/base/shop_database.sql
shopt -s nullglob
for dump in /sylvania/dumps/*.sql /sylvania/dumps/*.sql.gz; do
echo "Importing game database dump: ${dump}"
if [[ "${dump}" == *.gz ]]; then
gzip -dc "${dump}" | mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}"
gzip -dc "${dump}" | mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}"
else
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" < "${dump}"
mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" < "${dump}"
fi
done
# The published 7.3.5 dump uses a lowercase name, while the core queries this
# one legacy table with its historical mixed-case identifier on Linux.
mariadb --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" \
mariadb --default-character-set=utf8mb4 --protocol=socket -uroot -p"${MARIADB_ROOT_PASSWORD}" \
-e 'RENAME TABLE hotfixes.questv2clitask TO hotfixes.QuestV2CliTask;'
+7 -1
View File
@@ -15,6 +15,7 @@ external_address="${EXTERNAL_ADDRESS:-127.0.0.1}"
realm_name="${REALM_NAME:-Sylvania Solo}"
realm_address="${REALM_ADDRESS:-127.0.0.1}"
realm_port="${REALM_PORT:-8086}"
dbc_locale="${DBC_LOCALE:-8}"
if [[ ! "${realm_name}" =~ ^[[:alnum:]А-Яа-яЁё._[:space:]-]{1,32}$ ]]; then
echo "REALM_NAME contains unsupported characters or is longer than 32 characters" >&2
@@ -28,6 +29,10 @@ if [[ ! "${realm_port}" =~ ^[0-9]+$ ]] || (( realm_port < 1 || realm_port > 6553
echo "REALM_PORT must be an integer between 1 and 65535" >&2
exit 1
fi
if [[ ! "${dbc_locale}" =~ ^[0-9]+$ ]] || (( dbc_locale > 11 || dbc_locale == 9 )); then
echo "DBC_LOCALE must be a valid locale index (ruRU is 8)" >&2
exit 1
fi
config_dir=/opt/sylvania/etc
template="${config_dir}/${server}.conf.dist"
@@ -40,7 +45,7 @@ sed -i -E \
# Keep the client-facing realm record in sync after restores and migrations.
# Values are validated above before being interpolated into SQL.
mariadb --protocol=tcp \
mariadb --default-character-set=utf8mb4 --protocol=tcp \
-h "${db_host}" -P "${db_port}" -u "${db_user}" "-p${db_password}" auth <<SQL
INSERT INTO realmlist
(id, name, address, localAddress, localSubnetMask, port, icon, flag,
@@ -63,6 +68,7 @@ if [[ "${server}" == worldserver ]]; then
-e "s#^(HotfixDatabaseInfo[[:space:]]*=).*#\\1 \"${db_host};${db_port};${db_user};${db_password};hotfixes\"#" \
-e "s#^(ShopDatabaseInfo[[:space:]]*=).*#\\1 \"${db_host};${db_port};${db_user};${db_password};shop\"#" \
-e 's#^DataDir[[:space:]]*=.*#DataDir = "/opt/sylvania/data"#' \
-e "s#^DBC.Locale[[:space:]]*=.*#DBC.Locale = ${dbc_locale}#" \
-e 's#^LogsDir[[:space:]]*=.*#LogsDir = "/opt/sylvania/logs"#' \
"${config}"
else
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
-- Generated from ChatGPT WoW localisation source.csv.
-- Apply to dc_world. Existing frFR rows are not modified.
SET NAMES utf8mb4;
INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`) VALUES
(92718,0,0,'ruRU','Мне понадобится твоя помощь, чтобы остановить Гул''дана. Освободи остальных Иллидари, поскорее!'),
(92980,1,0,'ruRU','Демоны хотят уничтожить наш мир. Мы должны освободить наших союзников.'),
(92986,1,0,'ruRU','Объяснения могут подождать. Нужно убить демонов.'),
(93011,0,0,'ruRU','Вы слышали лорда Иллидана. Давайте найдем этот краеугольный камень.'),
(93011,1,0,'ruRU','С его помощью мы сможем вторгнуться на любую планету Легиона, даже на Аргус.'),
(93105,0,0,'ruRU','Мои глаза… Мои глаза!'),
(93105,2,0,'ruRU','Довольно! Мой адский колосс тебя раздавит.'),
(93117,0,0,'ruRU','Какая боль!'),
(93127,0,0,'ruRU','Их главные цели — Паучья крепость, Двигатель душ и Кузница. Слуги Иллидана должны добиться успеха.'),
(93127,1,0,'ruRU','Если эти эсминцы не будут уничтожены, мы долго не продержимся!'),
(93127,3,0,'ruRU','Я чувствую, как это течет по моим венам. Это преображает мое тело.'),
(93127,4,0,'ruRU','Нет… Маннетрель.'),
(93221,1,0,'ruRU','Вы не переживёте Адскую Вершину…'),
(93759,1,0,'ruRU','Используйте тигель для завершения ритуала.'),
(93802,0,0,'ruRU','Придите, дети мои. Насладитесь нашими врагами.'),
(93802,2,0,'ruRU','Лорд Саргерас, нет!!!'),
(94400,0,0,'ruRU','Я вас всех уничтожу.'),
(94400,1,0,'ruRU','Я убью каждого демона на своём пути.'),
(95226,1,0,'ruRU','Здесь вы — добыча.'),
(95226,3,0,'ruRU','В моей клетке, и даже быстрее.'),
(96277,0,0,'ruRU','Я тебя расчленю.'),
(96279,1,0,'ruRU','В моей клетке, и даже быстрее.'),
(96279,2,0,'ruRU','Так сильно они жаждут попасть в рабство.'),
(96402,1,0,'ruRU','Умри в славе битвы!'),
(96420,0,0,'ruRU','Нас в десять раз больше. Сможем ли мы действительно их победить?'),
(96436,1,0,'ruRU','Они от нас не скроются. Иллидари, атака!'),
(96441,1,0,'ruRU','Да будет проклято твое призрачное видение!'),
(96494,4,0,'ruRU','Вы смеете нападать на нас здесь?!'),
(96652,1,0,'ruRU','Аа ...'),
(97034,0,0,'ruRU','Я живу, чтобы служить.'),
(97034,1,0,'ruRU','Мой клинок пронзит тебя.'),
(97057,0,0,'ruRU','Тебе нравится мой новый меч? Подойди поближе.'),
(97057,1,0,'ruRU','Глупые мелочи…'),
(97058,0,0,'ruRU','Мой рой насытится твоей душой!'),
(97244,0,0,'ruRU','Забирайте краеугольный камень! Давайте направимся к воротам внизу и вернемся в Черный Храм!'),
(97370,1,0,'ruRU','От моего огня не останется ничего, кроме костей.'),
(97459,0,0,'ruRU','Мои силы иссякли. Я подвел тебя.'),
(97459,1,0,'ruRU','Клянусь отдать свою жизнь, если это потребуется.'),
(98229,0,0,'ruRU','Активируйте три портала и соберите остальные силы.'),
(98229,1,0,'ruRU','Я пробьюсь сквозь демонов и встречусь с тобой у вулкана.'),
(98292,0,0,'ruRU','Убейте их всех!'),
(98354,0,0,'ruRU','Я иду с тобой. Пойдём убьём ещё нескольких демонов.'),
(98459,0,0,'ruRU','Знаете, это я должен быть главным.'),
(98483,0,0,'ruRU','Для меня это вкусное, сочное мясо.'),
(98497,1,0,'ruRU','Мне скоро принесут еду.'),
(98497,3,0,'ruRU','Мои дети приготовят для вас идеальное блюдо.'),
(98497,4,0,'ruRU','Победа Легиона неизбежна, дитя моё.'),
(98497,5,0,'ruRU','Я сам доставлю тебя к Тиранне.'),
(98497,6,0,'ruRU','Грязный маленький эльф. Я чувствую вкус скверны на тебе.'),
(98711,0,0,'ruRU','Мать маленьких дьяволят находится в той пещере, прямо там.'),
(98986,0,0,'ruRU','Мои развращающие тайны поглотят тебя!'),
(98986,1,0,'ruRU','Они крадут наши секреты, моя королева…'),
(99045,1,0,'ruRU','Я мог бы в одиночку уничтожить Легион!'),
(99631,0,0,'ruRU','Я что, не сплю? Как это возможно?'),
(99632,0,0,'ruRU','Сколько лет я потратил впустую в этой камере?'),
(99917,1,0,'ruRU','Севис опускает взгляд на сломленного, умирающего мистика.'),
(99917,2,0,'ruRU','Встретимся у последнего пешеходного моста.'),
(100545,0,0,'ruRU','За лорда Иллидана!'),
(103432,0,0,'ruRU','Я тебя расчленю.')
ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);
INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`)
SELECT ct.`CreatureID`,ct.`GroupID`,ct.`ID`,'ruRU',REPLACE(REPLACE(ct.`Text`, 'begins to cast', 'начинает произносить'), '[Carrion Storm]', '[Буря Падали]')
FROM `creature_text` ct WHERE ct.`CreatureID`=97058 AND ct.`GroupID`=1 AND ct.`ID`=0
ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);
INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`)
SELECT ct.`CreatureID`,ct.`GroupID`,ct.`ID`,'ruRU',REPLACE(REPLACE(REPLACE(ct.`Text`, 'Nearby', 'Ближайшие'), '[Spider Eggs]', '[Паучьи яйца]'), 'will hatch soon if not destroyed!.', 'скоро вылупятся, если их не уничтожить!')
FROM `creature_text` ct WHERE ct.`CreatureID`=97059 AND ct.`GroupID`=2 AND ct.`ID`=0
ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);
INSERT INTO `gossip_menu_option_locale` (`MenuId`,`OptionIndex`,`Locale`,`OptionText`,`BoxText`) VALUES
(18776,0,'ruRU','Уайтмун, я возьму одну из твоих пораженных гангреной летучих мышей. Мы найдем то, за чем пришли.','')
ON DUPLICATE KEY UPDATE `OptionText`=VALUES(`OptionText`);
+1 -1
View File
@@ -4,6 +4,6 @@ UPDATE `realmlist`
SET `name` = 'FelWell',
`address` = 'legion.moon-well.online',
`localAddress` = 'legion.moon-well.online',
`port` = 8085,
`port` = 8086,
`gamebuild` = 26972
WHERE `id` = 1;
@@ -118,6 +118,7 @@ m_SeduceTarget(ObjectGuid::Empty),
m_MasterPlayer(NULL),
m_FullDispel(0),
m_MeleeFleeTick(0),
m_AttackDbgTick(0),
bothp(0),
m_ForceFlee(false),
m_StopFollow(false),
@@ -271,12 +272,12 @@ void BotGroupAI::ProcessAttackCommand()
}
if (!me->IsValidAttackTarget(pMasterTarget))
{
TC_LOG_ERROR("botai", "ATTACKDBG %s: cible %u (entree %u) refusee par IsValidAttackTarget.",
TC_LOG_ERROR("botai", "ATTACKDBG %s: cible " UI64FMTD " (entree %u) refusee par IsValidAttackTarget.",
me->GetName().c_str(), pMasterTarget->GetGUID().GetCounter(),
pMasterTarget->ToCreature() ? pMasterTarget->ToCreature()->GetEntry() : 0);
return;
}
TC_LOG_ERROR("botai", "ATTACKDBG %s: cible %u (entree %u) retenue, distance %.1f.",
TC_LOG_ERROR("botai", "ATTACKDBG %s: cible " UI64FMTD " (entree %u) retenue, distance %.1f.",
me->GetName().c_str(), pMasterTarget->GetGUID().GetCounter(),
pMasterTarget->ToCreature() ? pMasterTarget->ToCreature()->GetEntry() : 0,
me->GetDistance(pMasterTarget));
@@ -3049,7 +3050,7 @@ Unit* BotGroupAI::GetBotAIValidSelectedUnit()
else if (IsNotSelect(pTarget)) motif = "IsNotSelect";
else if (TargetIsControl(pTarget)) motif = "controle/evade";
else if (m_FliterCreatures.IsFliterCreature(pTarget->ToCreature())) motif = "filtre";
TC_LOG_ERROR("botai", "ATTACKDBG %s: cible %u rejetee -- %s.",
TC_LOG_ERROR("botai", "ATTACKDBG %s: cible " UI64FMTD " rejetee -- %s.",
me->GetName().c_str(), pTarget->GetGUID().GetCounter(), motif);
}
me->AttackStop();
@@ -224,6 +224,7 @@ protected:
Player* m_MasterPlayer;
uint32 m_FullDispel;
uint32 m_MeleeFleeTick;
uint32 m_AttackDbgTick;
uint32 bothp;
bool m_ForceFlee;
bool m_StopFollow;
@@ -83,9 +83,9 @@ char const* CapitalSiegeMgr::GetTeamName(TeamId team)
{
switch (team)
{
case TEAM_ALLIANCE: return "Alliance";
case TEAM_HORDE: return "Horde";
default: return "aucune";
case TEAM_ALLIANCE: return "Альянса";
case TEAM_HORDE: return "Орды";
default: return "неизвестной стороны";
}
}
@@ -208,7 +208,7 @@ void CapitalSiegeMgr::LoadConfig()
m_targets[TEAM_HORDE].bossLevel = sConfigMgr->GetIntDefault("siege_alliance_boss_level", 0);
m_targets[TEAM_HORDE].bossHealth = uint64(sConfigMgr->GetIntDefault("siege_alliance_boss_health", 0));
m_targets[TEAM_HORDE].bossFaction = sConfigMgr->GetIntDefault("siege_alliance_boss_faction", 0);
m_targets[TEAM_HORDE].cityName = "Hurlevent";
m_targets[TEAM_HORDE].cityName = "Штормград";
TC_LOG_INFO("server.loading", "Siege des Capitales: %s (plage %02uh-%02uh, %u bots niveau %u, duree max %u min).",
m_enabled ? "actif" : "inactif", m_hourMin, m_hourMax, m_botCount, m_botLevel, m_duration / MINUTE);
@@ -392,7 +392,7 @@ bool CapitalSiegeMgr::StartSiege(TeamId attacker, std::string const& trigger)
GetTeamName(attacker), m_target->cityName, trigger.c_str());
if (m_announce)
Announce("|cffff2020[Siege des Capitales]|r Une armee de la %s marche sur %s !", GetTeamName(attacker), m_target->cityName);
Announce("|cffff2020[Осада столиц]|r Армия %s выступила на %s!", GetTeamName(attacker), m_target->cityName);
// E7 : demarrage du spawn etale de la horde d invasion.
// E8 : eveil du dirigeant de la capitale.
@@ -431,10 +431,10 @@ void CapitalSiegeMgr::StopSiege(CapitalSiegeOutcome outcome)
if (m_announce)
{
if (outcome == SIEGE_OUTCOME_VICTORY)
Announce("|cffff2020[Siege des Capitales]|r %s est tombee : le dirigeant a ete abattu par la %s !",
m_target ? m_target->cityName : "La capitale", GetTeamName(m_attackerTeam));
Announce("|cffff2020[Осада столиц]|r %s пал: правитель убит армией %s!",
m_target ? m_target->cityName : "Столица", GetTeamName(m_attackerTeam));
else if (outcome == SIEGE_OUTCOME_TIMEOUT)
Announce("|cff20ff20[Siege des Capitales]|r L assaut sur %s a ete repousse.", m_target ? m_target->cityName : "la capitale");
Announce("|cff20ff20[Осада столиц]|r Штурм города %s отражён.", m_target ? m_target->cityName : "столица");
}
m_status = SIEGE_STATUS_IDLE;
+62
View File
@@ -993,6 +993,68 @@ void WorldSession::HandleSetRaidDifficultyOpcode(WorldPackets::Misc::SetRaidDiff
}
}
// Les raids d'avant Warlords ne passent pas par CMSG_SET_RAID_DIFFICULTY pour le mode
// heroique : le client envoie CMSG_TOGGLE_DIFFICULTY, sans rien preciser, et attend que
// le serveur bascule entre normal et heroique en conservant la taille du raid. Cet
// opcode n'etait pas traite du tout, si bien que le client repondait « vous ne remplissez
// pas les conditions requises » -- impossible de monter les Terres de feu en heroique.
void WorldSession::HandleToggleDifficultyOpcode(WorldPackets::Misc::ToggleDifficulty& /*toggleDifficulty*/)
{
Difficulty difficultyID;
switch (_player->GetLegacyRaidDifficultyID())
{
case DIFFICULTY_10_N: difficultyID = DIFFICULTY_10_HC; break;
case DIFFICULTY_25_N: difficultyID = DIFFICULTY_25_HC; break;
case DIFFICULTY_10_HC: difficultyID = DIFFICULTY_10_N; break;
case DIFFICULTY_25_HC: difficultyID = DIFFICULTY_25_N; break;
default:
return;
}
// Memes garde-fous que le changement de difficulte ordinaire : on ne bascule pas
// depuis l'interieur d'une instance, et seul le chef decide pour le groupe.
Map* map = _player->FindMap();
if (map && map->IsDungeon())
{
TC_LOG_DEBUG("network", "WorldSession::HandleToggleDifficultyOpcode: player (Name: %s, %s) tried to toggle the difficulty while inside an instance!",
_player->GetName().c_str(), _player->GetGUID().ToString().c_str());
return;
}
Group* group = _player->GetGroup();
if (group)
{
if (!group->IsLeader(_player->GetGUID()))
return;
for (GroupReference* itr = group->GetFirstMember(); itr != NULL; itr = itr->next())
{
Player* groupGuy = itr->GetSource();
if (!groupGuy)
continue;
if (!groupGuy->IsInMap(groupGuy))
return;
if (groupGuy->GetMap()->IsRaid())
{
TC_LOG_DEBUG("network", "WorldSession::HandleToggleDifficultyOpcode: %s tried to toggle the difficulty while group member (Name: %s, %s) is inside!",
_player->GetGUID().ToString().c_str(), groupGuy->GetName().c_str(), groupGuy->GetGUID().ToString().c_str());
return;
}
}
group->ResetInstances(INSTANCE_RESET_CHANGE_DIFFICULTY, true, true, _player);
group->SetLegacyRaidDifficultyID(difficultyID);
}
else
{
_player->ResetInstances(INSTANCE_RESET_CHANGE_DIFFICULTY, true, true);
_player->SetLegacyRaidDifficultyID(difficultyID);
_player->SendRaidDifficulty(true);
}
}
void WorldSession::HandleSetTaxiBenchmark(WorldPackets::Misc::SetTaxiBenchmarkMode& packet)
{
_player->ApplyModFlag(PLAYER_FLAGS, PLAYER_FLAGS_TAXI_BENCHMARK, packet.Enable);
+26 -26
View File
@@ -331,7 +331,7 @@ bool MercenaryChatMgr::HandleApiCommand(Player* owner, std::string const& argume
if (!m_enabled)
{
handler.PSendSysMessage("|cffff0000[Mercenaire]|r Le dialogue par intelligence artificielle est desactive sur ce royaume.");
handler.PSendSysMessage("|cffff0000[Наёмник]|r Диалоги с искусственным интеллектом отключены на этом сервере.");
return true;
}
@@ -341,28 +341,28 @@ bool MercenaryChatMgr::HandleApiCommand(Player* owner, std::string const& argume
if (first.empty() || first == "aide" || first == "help")
{
handler.PSendSysMessage("|cff00ff00[Mercenaire]|r Pour delier la langue de votre mercenaire, fournissez votre propre cle :");
handler.PSendSysMessage(" |cffffd100!api <fournisseur> <modele> <cle>|r");
handler.PSendSysMessage(" fournisseurs : |cffffd100mistral|r, |cffffd100anthropic|r, |cffffd100openai|r, |cffffd100gemini|r");
handler.PSendSysMessage(" exemple : |cffffd100!api mistral mistral-small-latest VOTRE_CLE|r");
handler.PSendSysMessage(" |cffff8800Gemini ne repond pas depuis ce royaume|r (Google refuse les serveurs).");
handler.PSendSysMessage(" service compatible OpenAI (Groq, OpenRouter...) : ajoutez l'adresse en fin de ligne.");
handler.PSendSysMessage(" |cffffd100!api off|r retire votre cle immediatement.");
handler.PSendSysMessage("Votre cle n'est jamais enregistree : elle vit en memoire et disparait a la rupture du contrat ou a votre deconnexion.");
handler.PSendSysMessage("|cff00ff00[Наёмник]|r Чтобы наёмник заговорил, укажите собственный ключ API:");
handler.PSendSysMessage(" |cffffd100!api <провайдер> <модель> <ключ>|r");
handler.PSendSysMessage(" провайдеры: |cffffd100mistral|r, |cffffd100anthropic|r, |cffffd100openai|r, |cffffd100gemini|r");
handler.PSendSysMessage(" пример: |cffffd100!api mistral mistral-small-latest ВАШ_КЛЮЧ|r");
handler.PSendSysMessage(" |cffff8800Gemini не отвечает с этого сервера|r (Google отклоняет запросы сервера).");
handler.PSendSysMessage(" OpenAI-совместимый сервис (Groq, OpenRouter...): добавьте адрес в конец команды.");
handler.PSendSysMessage(" |cffffd100!api off|r немедленно удаляет ваш ключ.");
handler.PSendSysMessage("Ключ нигде не сохраняется: он хранится только в памяти и исчезает при разрыве договора или выходе из игры.");
return true;
}
if (first == "off" || first == "stop" || first == "clear")
{
ClearCredential(owner->GetGUID());
handler.PSendSysMessage("|cff00ff00[Mercenaire]|r Votre cle a ete effacee. Vos mercenaires redeviennent muets.");
handler.PSendSysMessage("|cff00ff00[Наёмник]|r Ключ удалён. Ваши наёмники снова замолчали.");
return true;
}
uint8 const provider = ParseProvider(first);
if (provider == MERC_LLM_UNKNOWN)
{
handler.PSendSysMessage("|cffff0000[Mercenaire]|r Fournisseur inconnu. Attendus : openai, anthropic, gemini.");
handler.PSendSysMessage("|cffff0000[Наёмник]|r Неизвестный провайдер. Доступны: openai, anthropic, gemini.");
return true;
}
@@ -371,7 +371,7 @@ bool MercenaryChatMgr::HandleApiCommand(Player* owner, std::string const& argume
if (model.empty() || key.empty())
{
handler.PSendSysMessage("|cffff0000[Mercenaire]|r Il manque le modele ou la cle. Tapez |cffffd100!api|r pour l'aide.");
handler.PSendSysMessage("|cffff0000[Наёмник]|r Не указана модель или ключ. Введите |cffffd100!api|r для справки.");
return true;
}
@@ -391,9 +391,9 @@ bool MercenaryChatMgr::HandleApiCommand(Player* owner, std::string const& argume
// La longueur seule est renvoyee : de quoi verifier une faute de frappe sans
// jamais reafficher le secret.
handler.PSendSysMessage("|cff00ff00[Mercenaire]|r Cle acceptee (%s, modele %s, %u caracteres). Vos mercenaires peuvent desormais vous repondre.",
handler.PSendSysMessage("|cff00ff00[Наёмник]|r Ключ принят (%s, модель %s, %u символов). Теперь наёмники могут отвечать вам.",
GetProviderName(provider), model.c_str(), uint32(key.size()));
handler.PSendSysMessage("Parlez-leur normalement : chuchotement, canal de groupe ou a voix haute. Les ordres, eux, commencent par |cffffd100!|r.");
handler.PSendSysMessage("Общайтесь обычным сообщением: шёпотом, в группе или рядом. Приказы начинаются с |cffffd100!|r.");
TC_LOG_INFO("server.worldserver", "Mercenaires (dialogue): %s a fourni une cle %s (modele %s).",
owner->GetName().c_str(), GetProviderName(provider), model.c_str());
@@ -493,7 +493,7 @@ std::string MercenaryChatMgr::BuildSystemPrompt(Player* owner, Player* bot) cons
<< "Un portail magique t'a arrache a tes affaires contre cent pieces d'or : tu sers cet employeur "
<< "tant que dure le contrat, ni plus, ni moins.\n"
<< "PARLER : francais, une a trois phrases, jamais davantage. Registre medieval-fantastique, "
<< "PARLER : russe uniquement, une a trois phrases, jamais davantage. Registre medieval-fantastique, "
<< "rude et image, celui d'un soudard qui a vu trop de champs de bataille. "
<< "Tu tutoies ton employeur ; tu l'appelles parfois « patron », « chef » ou par son nom. "
<< ClassOaths(bot->getClass()) << " "
@@ -526,7 +526,7 @@ std::string MercenaryChatMgr::BuildSystemPrompt(Player* owner, Player* bot) cons
<< "te disent rien, et tu le dis franchement si on t en parle.";
prompt << "\nSITUATION : ";
LocaleConstant const locale = owner->GetSession() ? owner->GetSession()->GetSessionDbcLocale() : LOCALE_frFR;
LocaleConstant const locale = owner->GetSession() ? owner->GetSession()->GetSessionDbcLocale() : LOCALE_ruRU;
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetAreaId()))
{
prompt << "vous vous tenez a " << area->AreaName->Str[locale];
@@ -808,7 +808,7 @@ bool MercenaryChatMgr::PerformOnce(MercenaryChatRequest const& request, std::str
if (result != CURLE_OK)
{
error = std::string("Le lien s'est rompu (") + curl_easy_strerror(result) + ").";
error = std::string("Связь разорвана (") + curl_easy_strerror(result) + ").";
return false;
}
@@ -824,15 +824,15 @@ bool MercenaryChatMgr::PerformOnce(MercenaryChatRequest const& request, std::str
std::ostringstream text;
if (httpCode == 401 || httpCode == 403)
text << "Votre cle a ete refusee";
text << "Ваш ключ отклонён";
else if (httpCode == 404)
text << "Modele introuvable pour votre cle";
text << "Модель недоступна для вашего ключа";
else if (httpCode == 429)
text << "Votre fournisseur refuse le rythme";
text << "Провайдер ограничил частоту запросов";
else if (httpCode == 503 || httpCode == 502 || httpCode == 504)
text << "Votre fournisseur est surcharge, reessayez dans un instant";
text << "Провайдер перегружен, повторите попытку позже";
else
text << "Le fournisseur a repondu par une erreur";
text << "Провайдер вернул ошибку";
text << " (" << httpCode << ")";
if (!detail.empty())
@@ -848,7 +848,7 @@ bool MercenaryChatMgr::PerformOnce(MercenaryChatRequest const& request, std::str
answer = ExtractAnswer(request.provider, body, parseError);
if (answer.empty())
{
error = parseError.empty() ? "Reponse vide du fournisseur." : parseError;
error = parseError.empty() ? "Провайдер вернул пустой ответ." : parseError;
return false;
}
return true;
@@ -868,7 +868,7 @@ std::string MercenaryChatMgr::ExtractAnswer(uint8 provider, std::string const& b
std::string detail;
if (FindJsonString(body, "message", errorSection, detail) && !detail.empty())
{
error = "Le fournisseur refuse : " + detail;
error = "Провайдер отклонил запрос: " + detail;
return "";
}
}
@@ -890,14 +890,14 @@ std::string MercenaryChatMgr::ExtractAnswer(uint8 provider, std::string const& b
size_t const start = body.find(section);
if (start == std::string::npos)
{
error = "Reponse illisible du fournisseur.";
error = "Не удалось прочитать ответ провайдера.";
return "";
}
std::string answer;
if (!FindJsonString(body, key, start, answer))
{
error = "Reponse illisible du fournisseur.";
error = "Не удалось прочитать ответ провайдера.";
return "";
}
return answer;
+22 -22
View File
@@ -87,9 +87,9 @@ char const* MercenaryMgr::GetRoleName(uint8 role)
{
switch (role)
{
case ROLE_TANK: return "protecteur";
case ROLE_HEALER: return "guerisseur";
default: return "combattant";
case ROLE_TANK: return "защитник";
case ROLE_HEALER: return "лекарь";
default: return "боец";
}
}
@@ -97,16 +97,16 @@ char const* MercenaryMgr::GetErrorText(MercenaryResult result)
{
switch (result)
{
case MERC_ERR_DISABLED: return "Le portail est éteint. Aucun mercenaire ne répond à l'appel.";
case MERC_ERR_BAD_PLACE: return "Le portail ne peut percer le voile depuis un champ de bataille ou une arène.";
case MERC_ERR_IN_COMBAT: return "Impossible d'invoquer un mercenaire en plein combat.";
case MERC_ERR_LEVEL: return "Vous êtes trop inexpérimenté pour commander un mercenaire.";
case MERC_ERR_NOT_LEADER: return "Seul le chef du groupe peut invoquer un mercenaire.";
case MERC_ERR_GROUP_FULL: return "Votre groupe est déjà complet.";
case MERC_ERR_MAX_REACHED: return "Vous commandez déjà autant de mercenaires que le portail l'autorise.";
case MERC_ERR_NO_MONEY: return "Vous n'avez pas assez d'or pour payer ce mercenaire.";
case MERC_ERR_NO_BOT: return "Aucun mercenaire de cette spécialité n'est disponible pour le moment. Votre or ne vous a pas été prélevé.";
case MERC_ERR_PENDING: return "Une invocation est déjà en cours. Patientez.";
case MERC_ERR_DISABLED: return "Портал неактивен. Ни один наёмник не ответит на зов.";
case MERC_ERR_BAD_PLACE: return "Портал нельзя использовать на поле боя или арене.";
case MERC_ERR_IN_COMBAT: return "Нельзя призвать наёмника во время боя.";
case MERC_ERR_LEVEL: return "Ваш уровень слишком низок, чтобы командовать наёмником.";
case MERC_ERR_NOT_LEADER: return "Призвать наёмника может только лидер группы.";
case MERC_ERR_GROUP_FULL: return "Ваша группа уже заполнена.";
case MERC_ERR_MAX_REACHED: return "Вы уже призвали максимально допустимое число наёмников.";
case MERC_ERR_NO_MONEY: return "У вас недостаточно золота для найма.";
case MERC_ERR_NO_BOT: return "Сейчас нет свободного наёмника этой роли. Золото не списано.";
case MERC_ERR_PENDING: return "Призыв уже выполняется. Подождите.";
default: return "";
}
}
@@ -474,7 +474,7 @@ void MercenaryMgr::Refund(ObjectGuid ownerGuid, char const* reason)
return;
owner->ModifyMoney(int64(m_cost));
ChatHandler(owner->GetSession()).PSendSysMessage("|cff00ff00[Portail]|r %s Vos %u pièces d'or vous sont rendues.",
ChatHandler(owner->GetSession()).PSendSysMessage("|cff00ff00[Портал]|r %s Вам возвращено %u золотых.",
reason, GetCostGold());
}
@@ -527,7 +527,7 @@ void MercenaryMgr::DismissOne(ObjectGuid botGuid)
if (Player* owner = ObjectAccessor::FindConnectedPlayer(contract.ownerGuid))
ChatHandler(owner->GetSession()).PSendSysMessage(
"|cff00ff00[Portail]|r Votre %s retourne d'où il vient. Il vous faudra payer de nouveau pour en invoquer un autre.",
"|cff00ff00[Портал]|r Ваш %s возвращается обратно. Новый призыв потребуется оплатить.",
GetRoleName(contract.role));
ReleaseBot(contract);
@@ -620,7 +620,7 @@ void MercenaryMgr::Update(uint32 diff)
if (Player* owner = ObjectAccessor::FindConnectedPlayer(contract.ownerGuid))
ChatHandler(owner->GetSession()).PSendSysMessage(
"|cff00ff00[Portail]|r Votre %s retourne d'où il vient. Il vous faudra payer de nouveau pour en invoquer un autre.",
"|cff00ff00[Портал]|r Ваш %s возвращается обратно. Новый призыв потребуется оплатить.",
GetRoleName(contract.role));
ReleaseBot(contract);
@@ -679,7 +679,7 @@ void MercenaryMgr::Update(uint32 diff)
// perdu qu un mercenaire qu on croit present.
it->summonPending = false;
ChatHandler(owner->GetSession()).PSendSysMessage(
"|cffff4444[Portail]|r %s n'a pas pu vous rejoindre.", bot->GetName().c_str());
"|cffff4444[Портал]|r %s не смог добраться до вас.", bot->GetName().c_str());
TC_LOG_ERROR("server.worldserver",
"Mercenaires: %s n'a pas rejoint %s apres %u rappels (carte %u).",
bot->GetName().c_str(), owner->GetName().c_str(),
@@ -710,7 +710,7 @@ void MercenaryMgr::Update(uint32 diff)
PlayerBotSession* session = dynamic_cast<PlayerBotSession*>(worldSession);
if (!session)
{
Refund(it->ownerGuid, "Le mercenaire ne s'est jamais présenté.");
Refund(it->ownerGuid, "Наёмник так и не появился.");
it = m_contracts.erase(it);
continue;
}
@@ -725,7 +725,7 @@ void MercenaryMgr::Update(uint32 diff)
{
MercenaryContract const contract = *it;
it = m_contracts.erase(it);
Refund(contract.ownerGuid, "Le mercenaire ne s'est jamais présenté.");
Refund(contract.ownerGuid, "Наёмник так и не появился.");
ReleaseBot(contract);
continue;
}
@@ -744,7 +744,7 @@ void MercenaryMgr::Update(uint32 diff)
delete group;
MercenaryContract const contract = *it;
it = m_contracts.erase(it);
Refund(contract.ownerGuid, "Le pacte n'a pas pu être scellé.");
Refund(contract.ownerGuid, "Не удалось заключить договор.");
ReleaseBot(contract);
continue;
}
@@ -755,7 +755,7 @@ void MercenaryMgr::Update(uint32 diff)
{
MercenaryContract const contract = *it;
it = m_contracts.erase(it);
Refund(contract.ownerGuid, "Votre groupe est complet.");
Refund(contract.ownerGuid, "Ваша группа заполнена.");
ReleaseBot(contract);
continue;
}
@@ -827,7 +827,7 @@ void MercenaryMgr::Update(uint32 diff)
}
ChatHandler(owner->GetSession()).PSendSysMessage(
"|cff00ff00[Portail]|r %s, %s mercenaire, répond à votre appel.",
"|cff00ff00[Портал]|r %s, наёмник роли «%s», отвечает на ваш зов.",
bot->GetName().c_str(), GetRoleName(it->role));
TC_LOG_INFO("server.worldserver", "Mercenaires: %s rejoint le groupe de %s (role %s).",
@@ -300,6 +300,16 @@ namespace WorldPackets
uint8 Legacy = 0;
};
// Le bouton « heroique » des raids d'avant Warlords : aucune difficulte n'est
// transmise, le client demande simplement la bascule normal <-> heroique.
class ToggleDifficulty final : public ClientPacket
{
public:
ToggleDifficulty(WorldPacket&& packet) : ClientPacket(CMSG_TOGGLE_DIFFICULTY, std::move(packet)) { }
void Read() override { }
};
class DungeonDifficultySet final : public ServerPacket
{
public:
+1 -1
View File
@@ -830,7 +830,7 @@ void OpcodeTable::Initialize()
DEFINE_HANDLER(CMSG_TIME_SYNC_RESPONSE, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleTimeSyncResponse);
DEFINE_HANDLER(CMSG_TIME_SYNC_RESPONSE_DROPPED, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_TIME_SYNC_RESPONSE_FAILED, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_TOGGLE_DIFFICULTY, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_TOGGLE_DIFFICULTY, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleToggleDifficultyOpcode);
DEFINE_HANDLER(CMSG_TOGGLE_PVP, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleTogglePvP);
DEFINE_HANDLER(CMSG_TOTEM_DESTROYED, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleTotemDestroyed);
DEFINE_HANDLER(CMSG_TRADE_SKILL_SET_FAVORITE, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
+2
View File
@@ -569,6 +569,7 @@ namespace WorldPackets
class TutorialSetFlag;
class SetDungeonDifficulty;
class SetRaidDifficulty;
class ToggleDifficulty;
class PortGraveyard;
class ReclaimCorpse;
class RepopRequest;
@@ -1727,6 +1728,7 @@ class TC_GAME_API WorldSession
void HandleFarSightOpcode(WorldPackets::Misc::FarSight& packet);
void HandleSetDungeonDifficultyOpcode(WorldPackets::Misc::SetDungeonDifficulty& setDungeonDifficulty);
void HandleSetRaidDifficultyOpcode(WorldPackets::Misc::SetRaidDifficulty& setRaidDifficulty);
void HandleToggleDifficultyOpcode(WorldPackets::Misc::ToggleDifficulty& toggleDifficulty);
void HandleSetTitleOpcode(WorldPackets::Character::SetTitle& packet);
void HandleTimeSyncResponse(WorldPackets::Misc::TimeSyncResponse& packet);
void HandleDiscardedTimeSyncAcks(WorldPackets::Misc::DiscardedTimeSyncAcks& packet);
+1 -1
View File
@@ -21,7 +21,7 @@ public:
// Announce Module
if (sConfigMgr->GetBoolDefault("SoloLFG.Announce", true))
{
ChatHandler(player->GetSession()).SendSysMessage("Service |cff4CFF00Solo LFG|r running.");
ChatHandler(player->GetSession()).SendSysMessage("Сервис |cff4CFF00Solo LFG|r запущен.");
}
}
};
+4 -4
View File
@@ -45,13 +45,13 @@ class npc_rate_xp_modifier : public CreatureScript
continue;
std::ostringstream gossipText;
gossipText << "Taux d'expérience x" << i;
gossipText << "Рейт опыта x" << i;
AddGossipItemFor(player, GOSSIP_ICON_CHAT, gossipText.str(), GOSSIP_SENDER_MAIN, i);
}
// Toujours proposer le retour au taux par défaut du royaume (x1, Blizzlike)
std::ostringstream defaultText;
defaultText << "Revenir au taux par défaut (x" << defaultRate << ")";
defaultText << "Вернуться к рейту по умолчанию (x" << defaultRate << ")";
AddGossipItemFor(player, GOSSIP_ICON_CHAT, defaultText.str(), GOSSIP_SENDER_MAIN, 0);
SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID());
@@ -66,9 +66,9 @@ class npc_rate_xp_modifier : public CreatureScript
player->SetPersonnalXpRate(float(newRate));
if (newRate)
ChatHandler(player->GetSession()).PSendSysMessage("Votre taux d'expérience personnel est désormais x%u.", newRate);
ChatHandler(player->GetSession()).PSendSysMessage("Ваш персональный рейт опыта теперь x%u.", newRate);
else
ChatHandler(player->GetSession()).PSendSysMessage("Votre taux d'expérience est revenu à la valeur par défaut du royaume (x%g).", sWorld->getRate(RATE_XP_QUEST));
ChatHandler(player->GetSession()).PSendSysMessage("Ваш рейт опыта возвращён к значению мира по умолчанию (x%g).", sWorld->getRate(RATE_XP_QUEST));
return true;
}
+7 -7
View File
@@ -32,7 +32,7 @@ public:
{
if (!args || !*args)
{
handler->SendSysMessage("Usage: .qtest <nomJoueur> <questId> [complete]");
handler->SendSysMessage("Использование: .qtest <имяИгрока> <questId> [complete]");
handler->SetSentErrorMessage(true);
return false;
}
@@ -45,7 +45,7 @@ public:
if (name.empty() || questId == 0)
{
handler->SendSysMessage("Usage: .qtest <nomJoueur> <questId> [complete]");
handler->SendSysMessage("Использование: .qtest <имяИгрока> <questId> [complete]");
handler->SetSentErrorMessage(true);
return false;
}
@@ -53,7 +53,7 @@ public:
Player* player = ObjectAccessor::FindConnectedPlayerByName(name);
if (!player)
{
handler->PSendSysMessage("qtest: joueur '%s' introuvable (doit etre en ligne).", name.c_str());
handler->PSendSysMessage("qtest: игрок '%s' не найден (он должен быть в сети).", name.c_str());
handler->SetSentErrorMessage(true);
return false;
}
@@ -61,7 +61,7 @@ public:
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
{
handler->PSendSysMessage("qtest: quete %u introuvable.", questId);
handler->PSendSysMessage("qtest: задание %u не найдено.", questId);
handler->SetSentErrorMessage(true);
return false;
}
@@ -78,10 +78,10 @@ public:
default: stName = "?"; break;
}
handler->PSendSysMessage("=== qtest joueur=%s quete=%u statut=%s ===", name.c_str(), questId, stName);
handler->PSendSysMessage("=== qtest игрок=%s задание=%u статус=%s ===", name.c_str(), questId, stName);
QuestObjectives const& objs = quest->GetObjectives();
handler->PSendSysMessage("Objectifs: %u", uint32(objs.size()));
handler->PSendSysMessage("Цели: %u", uint32(objs.size()));
for (QuestObjective const& obj : objs)
{
int32 cur = player->GetQuestObjectiveData(quest, obj.StorageIndex);
@@ -95,7 +95,7 @@ public:
if (st == QUEST_STATUS_NONE && player->CanAddQuest(quest, false))
player->AddQuestAndCheckCompletion(quest, nullptr);
player->ForceCompleteQuest(questId);
handler->PSendSysMessage("qtest: ForceCompleteQuest applique -> statut=%d", int32(player->GetQuestStatus(questId)));
handler->PSendSysMessage("qtest: ForceCompleteQuest применён -> статус=%d", int32(player->GetQuestStatus(questId)));
}
return true;
+2 -2
View File
@@ -218,7 +218,7 @@ namespace {
player->GetName().c_str(), previous.statPct, previous.weaponPct, (uint32)player->GetMaxHealth());
if (announce && sConfigMgr->GetBoolDefault("Solocraft.Announce", false) && player->GetSession())
ChatHandler(player->GetSession()).PSendSysMessage("|cffFF0000[SoloCraft]|r |cffFF8000Bonus retiré (x%0.2f).|r",
ChatHandler(player->GetSession()).PSendSysMessage("|cffFF0000[SoloCraft]|r |cffFF8000Бонус отключён (x%0.2f).|r",
previous.effective);
return true;
@@ -273,7 +273,7 @@ namespace {
if (announce && sConfigMgr->GetBoolDefault("Solocraft.Announce", false) && player->GetSession())
{
ChatHandler(player->GetSession()).PSendSysMessage(
"|cffFF0000[SoloCraft]|r |cffFF8000%s - joueurs présents : %u - bonus x%0.2f (dégâts d'arme x%0.2f).|r",
"|cffFF0000[SoloCraft]|r |cffFF8000%s — игроков: %u — бонус x%0.2f (урон оружия x%0.2f).|r",
map->GetMapName(), numPlayers, buff.effective, 1.0f + buff.weaponPct / 100.0f);
}
}
@@ -115,7 +115,7 @@ class boss_nalorakk : public CreatureScript
{
boss_nalorakkAI(Creature* creature) : BossAI(creature, DATA_NALORAKK),
_bearForm(false), _waveEvent(true), _waveInProgress(false), _inMove(false),
_wave(0), _currentPoint(0), _targetPoint(0), _checkTimer(0) { }
_wave(0), _currentPoint(0), _targetPoint(0), _checkTimer(0), _moveTimeout(0) { }
void Reset() override
{
@@ -134,6 +134,7 @@ class boss_nalorakk : public CreatureScript
_currentPoint = 0;
_targetPoint = 0;
_checkTimer = 0;
_moveTimeout = 0;
_waveGuids.clear();
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE | UNIT_FLAG_NON_ATTACKABLE);
@@ -284,13 +285,15 @@ class boss_nalorakk : public CreatureScript
: (_targetPoint == 8 ? 8 : _targetPoint);
_currentPoint = next;
_moveTimeout = 20000;
me->GetMotionMaster()->MovePoint(_currentPoint, NalorakkWay[_currentPoint][0], NalorakkWay[_currentPoint][1], NalorakkWay[_currentPoint][2]);
}
void MovementInform(uint32 type, uint32 id) override
// Arrivee a une etape -- soit annoncee par le moteur de deplacement, soit
// forcee par le chrono ci-dessous quand la montee se bloque.
void OnReachedPoint()
{
if (!_waveEvent || type != POINT_MOTION_TYPE || !_inMove || _currentPoint != id)
return;
_moveTimeout = 0;
if (_currentPoint == _targetPoint)
{
@@ -302,6 +305,14 @@ class boss_nalorakk : public CreatureScript
AdvanceOnePoint();
}
void MovementInform(uint32 type, uint32 id) override
{
if (!_waveEvent || type != POINT_MOTION_TYPE || !_inMove || _currentPoint != id)
return;
OnReachedPoint();
}
void EnterCombat(Unit* /*who*/) override
{
Talk(SAY_AGGRO);
@@ -350,6 +361,21 @@ class boss_nalorakk : public CreatureScript
void UpdateAI(uint32 diff) override
{
// Le trajet entre deux paliers passe par un escalier bati en VMAP : si le
// calcul de chemin cale, le boss reste plante en route et l'evenement ne
// repart jamais. Au-dela de vingt secondes on le pose d'office sur l'etape.
if (_inMove && _moveTimeout)
{
if (_moveTimeout <= diff)
{
me->GetMotionMaster()->Clear(false);
me->NearTeleportTo(NalorakkWay[_currentPoint][0], NalorakkWay[_currentPoint][1], NalorakkWay[_currentPoint][2], me->GetOrientation());
OnReachedPoint();
}
else
_moveTimeout -= diff;
}
// Deroulement des vagues : il ne recule qu'une fois la vague abattue.
if (_waveInProgress)
{
@@ -447,6 +473,7 @@ class boss_nalorakk : public CreatureScript
uint32 _currentPoint;
uint32 _targetPoint;
uint32 _checkTimer;
uint32 _moveTimeout;
std::vector<ObjectGuid> _waveGuids;
};
@@ -532,6 +532,20 @@ class boss_alysrazor : public CreatureScript
instance->SendEncounterUnit(ENCOUNTER_FRAME_ENGAGE, me);
}
// Elle meurt en vol. La ramener dans JustDied ne suffisait pas : la chute est
// deja engagee et MoveFall vise le terrain (la lave, vers z=48) en traversant
// la plateforme, qui n'est qu'une structure VMAP a 55,5. On la pose donc au
// centre de l'arene AVANT le coup fatal, et sans declencher de chute.
void DamageTaken(Unit* /*attacker*/, uint32& damage) override
{
if (damage >= me->GetHealth())
{
me->SetCanFly(false);
me->SetDisableGravity(false);
me->NearTeleportTo(centerPos.GetPositionX(), centerPos.GetPositionY(), centerPos.GetPositionZ(), centerPos.GetOrientation());
}
}
void JustDied(Unit* /*killer*/) override
{
_JustDied();
@@ -539,11 +553,7 @@ class boss_alysrazor : public CreatureScript
RemoveEncounterAuras();
me->SetCanFly(false);
me->SetDisableGravity(false);
// Elle meurt en vol : laissee a elle-meme, la depouille traverse le vide
// entre les plateformes et finit dans la lave, butin compris. On la ramene
// au centre de l'arene avant la chute, comme en jeu officiel.
me->NearTeleportTo(centerPos.GetPositionX(), centerPos.GetPositionY(), centerPos.GetPositionZ(), centerPos.GetOrientation());
me->GetMotionMaster()->MoveFall();
}
void KilledUnit(Unit* who) override
@@ -300,9 +300,11 @@ class boss_lord_rhyolith : public CreatureScript
void UpdateAI(uint32 diff) override
{
if (!UpdateVictim())
return;
// Les joueurs frappent les pieds, jamais le colosse lui-meme : il lui
// arrive de n'avoir aucune cible, et on sortait alors d'UpdateAI avant
// meme d'avoir teste la bascule en phase 2. Le boss restait plante en
// premiere forme a un point de vie. Le passage de phase passe donc en
// premier, avant toute verification de cible.
if ((instance->GetData(DATA_RHYOLITH_HEALTH_SHARED) != 0))
me->SetHealth(instance->GetData(DATA_RHYOLITH_HEALTH_SHARED) * 2);
@@ -350,6 +352,9 @@ class boss_lord_rhyolith : public CreatureScript
return;
}
if (!UpdateVictim())
return;
events.Update(diff);
if (me->HasUnitState(UNIT_STATE_CASTING))
@@ -61,9 +61,9 @@ class npc_mercenary_portal : public CreatureScript
static void WhisperPitch(Creature* creature, Player* player)
{
std::ostringstream pitch;
pitch << "Lance " << sMercenaryMgr->GetCostGold() << " pièces d'or dans le portail et un mercenaire répondra à ton appel. "
<< "Jusqu'à " << sMercenaryMgr->GetMaxPerPlayer() << ", de quoi former un groupe complet. "
<< "Mais sache-le : le lien est fragile.";
pitch << "Бросьте " << sMercenaryMgr->GetCostGold() << " золотых в портал, и на ваш зов явится наёмник. "
<< "Можно призвать до " << sMercenaryMgr->GetMaxPerPlayer() << " наёмников — достаточно для полной группы. "
<< "Но помните: связь непрочна.";
creature->Whisper(pitch.str(), LANG_UNIVERSAL, player);
}
@@ -82,11 +82,11 @@ class npc_mercenary_portal : public CreatureScript
static void SendOrdersMenu(Player* player, Creature* creature)
{
ClearGossipMenuFor(player);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Les faire bouger", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_MOVE);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Les faire combattre", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_FIGHT);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "N'en désigner qu'un seul, ou un rôle", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_WHO);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Leur équipement et leur spécialisation", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_GEAR);
AddGossipItemFor(player, GOSSIP_ICON_TALK, "Revenir au portail", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_MAIN);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Управление движением", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_MOVE);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Управление боем", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_FIGHT);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Выбор одного наёмника или роли", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_WHO);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Экипировка и специализация", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS_GEAR);
AddGossipItemFor(player, GOSSIP_ICON_TALK, "Вернуться к порталу", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_MAIN);
SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID());
}
@@ -102,7 +102,7 @@ class npc_mercenary_portal : public CreatureScript
if (!sMercenaryMgr->IsEnabled())
{
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Le portail est éteint.", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Портал неактивен.", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID());
return true;
}
@@ -110,31 +110,31 @@ class npc_mercenary_portal : public CreatureScript
if (hired < maximum)
{
std::ostringstream tank;
tank << "Invoquer un protecteur (" << cost << " pièces d'or)";
tank << "Призвать защитника (" << cost << " золотых)";
AddGossipItemFor(player, GOSSIP_ICON_MONEY_BAG, tank.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_SUMMON_TANK);
std::ostringstream healer;
healer << "Invoquer un guérisseur (" << cost << " pièces d'or)";
healer << "Призвать лекаря (" << cost << " золотых)";
AddGossipItemFor(player, GOSSIP_ICON_MONEY_BAG, healer.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_SUMMON_HEAL);
std::ostringstream damage;
damage << "Invoquer un combattant (" << cost << " pièces d'or)";
damage << "Призвать бойца (" << cost << " золотых)";
AddGossipItemFor(player, GOSSIP_ICON_MONEY_BAG, damage.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_SUMMON_DPS);
}
else
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Le portail ne peut plus rien pour vous : votre compagnie est au complet.",
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Портал больше не может помочь: ваш отряд укомплектован.",
GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
if (hired)
{
std::ostringstream dismiss;
dismiss << "Congédier mes mercenaires (" << hired << "/" << maximum << ")";
dismiss << "Распустить моих наёмников (" << hired << "/" << maximum << ")";
AddGossipItemFor(player, GOSSIP_ICON_TALK, dismiss.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_DISMISS_ALL);
}
AddGossipItemFor(player, GOSSIP_ICON_TRAINER, "Comment leur donner des ordres ?", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Quelles sont les règles de ce pacte ?", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_EXPLAIN);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Rien pour l'instant.", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
AddGossipItemFor(player, GOSSIP_ICON_TRAINER, "Как отдавать им приказы?", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ORDERS);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Каковы условия договора?", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_EXPLAIN);
AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Пока ничего.", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID());
return true;
@@ -150,8 +150,8 @@ class npc_mercenary_portal : public CreatureScript
case GOSSIP_ACTION_SUMMON_DPS: role = ROLE_DAMAGE; break;
case GOSSIP_ACTION_EXPLAIN:
CloseGossipMenuFor(player);
creature->Whisper("Le mercenaire te suit tant que le lien tient. Quitte le groupe, renvoie-le, ou "
"déconnecte-toi, et il retourne au néant sur-le-champ. Chaque nouvelle invocation se paie, sans exception.",
creature->Whisper("Наёмник следует за вами, пока действует связь. Покиньте группу, отпустите его или "
"выйдите из игры — и он сразу вернётся в пустоту. Каждый новый призыв оплачивается заново.",
LANG_UNIVERSAL, player);
return true;
case GOSSIP_ACTION_ORDERS:
@@ -163,13 +163,13 @@ class npc_mercenary_portal : public CreatureScript
{
static char const* const lines[] =
{
"|cff00ff00[Mercenaires - les faire bouger]|r",
"Écrivez l'ordre dans le canal de groupe : il s'adresse à toute la compagnie, et il faut en être le chef.",
"Pour n'en viser qu'un : |cffffd100/w <nom du mercenaire> <ordre>|r",
" |cffffd100summon|r - il vous rejoint sur-le-champ, où que vous soyez",
" |cffffd100follow|r - il reprend sa marche derrière vous",
" |cffffd100stop|r - il s'arrête et ne bouge plus",
" |cffffd100flee|r - il rompt le combat et se replie"
"|cff00ff00[Наёмники — движение]|r",
"Введите приказ в канал группы: он действует на весь отряд, а вы должны быть лидером.",
"Чтобы обратиться к одному: |cffffd100/w <имя наёмника> <приказ>|r",
" |cffffd100summon|r — немедленно присоединиться к вам",
" |cffffd100follow|r — снова следовать за вами",
" |cffffd100stop|r — остановиться и не двигаться",
" |cffffd100flee|r — выйти из боя и отступить"
};
SendLines(player, lines, sizeof(lines) / sizeof(lines[0]));
SendOrdersMenu(player, creature);
@@ -179,12 +179,12 @@ class npc_mercenary_portal : public CreatureScript
{
static char const* const lines[] =
{
"|cff00ff00[Mercenaires - les faire combattre]|r",
" |cffffd100attack|r - il attaque la cible que vous avez sélectionnée",
" |cffffd100flee|r - il décroche et cesse le combat",
" |cffffd100rite|r - il lance son rituel d'invocation, si sa classe en possède un et que vous êtes à portée",
" |cffffd100<numéro de sort>|r - il lance ce sort précis s'il le connaît : tapez son identifiant",
"Sans ordre, un mercenaire se bat de lui-même : il choisit ses cibles, soigne et suit sa spécialisation."
"|cff00ff00[Наёмники — бой]|r",
" |cffffd100attack|r — атаковать выбранную вами цель",
" |cffffd100flee|r — прекратить бой и отступить",
" |cffffd100rite|r — применить ритуал призыва, если он доступен классу и вы рядом",
" |cffffd100<номер заклинания>|r — применить известное заклинание по его ID",
"Без приказа наёмник сражается самостоятельно: выбирает цели, лечит и следует своей специализации."
};
SendLines(player, lines, sizeof(lines) / sizeof(lines[0]));
SendOrdersMenu(player, creature);
@@ -194,13 +194,13 @@ class npc_mercenary_portal : public CreatureScript
{
static char const* const lines[] =
{
"|cff00ff00[Mercenaires - n'en désigner qu'un, ou un rôle]|r",
"Dans le canal de groupe, faites précéder l'ordre d'une cible :",
" par rôle : |cffffd100@tank|r |cffffd100@heal|r |cffffd100@dps|r |cffffd100@melee|r |cffffd100@ranged|r",
" par classe : |cffffd100@zs|r guerrier, |cffffd100@qs|r paladin, |cffffd100@lr|r chasseur, |cffffd100@dz|r voleur, |cffffd100@ms|r prêtre",
" |cffffd100@dk|r chevalier de la mort, |cffffd100@sm|r chaman, |cffffd100@fs|r mage, |cffffd100@ss|r démoniste, |cffffd100@xd|r druide",
"Exemple : |cffffd100@heal stop|r - seuls les guérisseurs s'arrêtent.",
"Sans préfixe, l'ordre vaut pour toute la compagnie."
"|cff00ff00[Наёмники — выбор цели приказа]|r",
"В канале группы добавьте перед приказом адресата:",
" по роли: |cffffd100@tank|r |cffffd100@heal|r |cffffd100@dps|r |cffffd100@melee|r |cffffd100@ranged|r",
" по классу: |cffffd100@zs|r воин, |cffffd100@qs|r паладин, |cffffd100@lr|r охотник, |cffffd100@dz|r разбойник, |cffffd100@ms|r жрец",
" |cffffd100@dk|r рыцарь смерти, |cffffd100@sm|r шаман, |cffffd100@fs|r маг, |cffffd100@ss|r чернокнижник, |cffffd100@xd|r друид",
"Пример: |cffffd100@heal stop|r — остановятся только лекари.",
"Без префикса приказ действует на весь отряд."
};
SendLines(player, lines, sizeof(lines) / sizeof(lines[0]));
SendOrdersMenu(player, creature);
@@ -210,17 +210,17 @@ class npc_mercenary_portal : public CreatureScript
{
static char const* const lines[] =
{
"|cff00ff00[Mercenaires - équipement et spécialisation]|r",
"Collez le lien de l'objet (Maj + clic sur l'objet) à la suite de l'ordre :",
" |cffffd100c|r - il énumère ce qu'il transporte",
" |cffffd100e <objet>|r - il l'équipe",
" |cffffd100ue <objet>|r - il le retire",
" |cffffd100s <objet>|r - il vous le remet",
" |cffffd100u <objet>|r - il l'utilise",
" |cffffd100destroy <objet>|r - il le détruit",
" |cffffd100talent 1|r, |cffffd1002|r ou |cffffd1003|r - il change de spécialisation",
" |cffffd100setting|r - il se remet à votre niveau et se rééquipe",
"Ces ordres-là sont refusés tant que le mercenaire est en combat."
"|cff00ff00[Наёмники — экипировка и специализация]|r",
"После приказа вставьте ссылку на предмет (Shift + щелчок):",
" |cffffd100c|r — показать содержимое сумок",
" |cffffd100e <предмет>|r — экипировать",
" |cffffd100ue <предмет>|r — снять",
" |cffffd100s <предмет>|r — передать вам",
" |cffffd100u <предмет>|r — использовать",
" |cffffd100destroy <предмет>|r — уничтожить",
" |cffffd100talent 1|r, |cffffd1002|r или |cffffd1003|r — сменить специализацию",
" |cffffd100setting|r — подстроить уровень и обновить экипировку",
"Эти приказы недоступны, пока наёмник находится в бою."
};
SendLines(player, lines, sizeof(lines) / sizeof(lines[0]));
SendOrdersMenu(player, creature);
@@ -230,7 +230,7 @@ class npc_mercenary_portal : public CreatureScript
CloseGossipMenuFor(player);
sMercenaryMgr->DismissAll(player->GetGUID());
ChatHandler(player->GetSession()).PSendSysMessage(
"|cff00ff00[Portail]|r Vos mercenaires sont congédiés. Toute nouvelle invocation devra être payée.");
"|cff00ff00[Портал]|r Наёмники распущены. Каждый новый призыв потребуется оплатить.");
return true;
default:
CloseGossipMenuFor(player);
@@ -242,13 +242,13 @@ class npc_mercenary_portal : public CreatureScript
MercenaryResult const result = sMercenaryMgr->Summon(player, role, creature);
if (result != MERC_OK)
{
ChatHandler(player->GetSession()).PSendSysMessage("|cffff0000[Portail]|r %s",
ChatHandler(player->GetSession()).PSendSysMessage("|cffff0000[Портал]|r %s",
MercenaryMgr::GetErrorText(result));
return true;
}
ChatHandler(player->GetSession()).PSendSysMessage(
"|cff00ff00[Portail]|r %u pièces d'or franchissent le voile. Votre %s ne va pas tarder...",
"|cff00ff00[Портал]|r %u золотых исчезают за завесой. Ваш %s скоро прибудет...",
sMercenaryMgr->GetCostGold(), MercenaryMgr::GetRoleName(role));
return true;
+1 -1
View File
@@ -680,7 +680,7 @@ StrictPetNames = 0
# 10 - (ptBR)
# 11 - (itIT)
DBC.Locale = 0
DBC.Locale = 8
#
# DeclinedNames
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Build ruRU locale overlays from reviewed French-to-Russian CSV rows."""
from __future__ import annotations
import csv
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE_CSV = ROOT / "tools" / "ChatGPT WoW localisation source.csv"
OUT_DIR = ROOT / "sql" / "sylvania" / "locales"
INVALID = {"", "Загрузка..."}
BROADCAST_ROW = re.compile(
r"^\((\d+),'frFR','((?:''|[^'])*)','((?:''|[^'])*)',(\d+)\)(?:,|;)$"
)
TMP_BROADCAST_ROW = re.compile(r"^\((\d+), '((?:''|[^'])*)'\)(?:,|;)$")
CREATURE_ROW = re.compile(
r"^\((\d+),(\d+),(\d+),'frFR','((?:''|[^'])*)'\)(?:,|;)$"
)
def sql_quote(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def translations() -> dict[str, str]:
result: dict[str, str] = {}
with SOURCE_CSV.open(encoding="utf-8-sig", newline="") as source:
for row in csv.DictReader(source):
russian = row["russian_text"].strip()
if row["kind"] == "literal" and russian not in INVALID:
result[row["french_text"]] = russian
return result
def build_hotfixes(translated: dict[str, str]) -> int:
rows: dict[int, tuple[str, str, int]] = {}
dynamic_rows: dict[int, str] = {}
source = ROOT / "sql/sylvania/broadcast_text_frFR_client.sql"
for line in source.read_text(encoding="utf-8").splitlines():
match = BROADCAST_ROW.match(line)
if not match:
continue
entry, male, female, build = match.groups()
male_ru = translated.get(male, "") if male else ""
female_ru = translated.get(female, "") if female else ""
if male_ru or female_ru:
rows[int(entry)] = (male_ru, female_ru, int(build))
source = ROOT / "sql/sylvania/mardum_fr_broadcast.sql"
for line in source.read_text(encoding="utf-8").splitlines():
match = TMP_BROADCAST_ROW.match(line)
if not match or match.group(2) not in translated:
continue
entry = int(match.group(1))
dynamic_rows[entry] = translated[match.group(2)]
output = OUT_DIR / "ruRU_from_french_hotfixes.sql"
with output.open("w", encoding="utf-8", newline="\n") as sql:
sql.write("-- Generated from ChatGPT WoW localisation source.csv.\n")
sql.write("-- Apply to dc_hotfixes. Existing frFR rows are not modified.\n\n")
sql.write("SET NAMES utf8mb4;\n\n")
sql.write("INSERT INTO `broadcast_text_locale` "
"(`ID`,`locale`,`Text_lang`,`Text1_lang`,`VerifiedBuild`) VALUES\n")
values = []
for entry, (male, female, build) in sorted(rows.items()):
values.append(
f"({entry},'ruRU',{sql_quote(male)},{sql_quote(female)},{build})"
)
sql.write(",\n".join(values))
sql.write("\nON DUPLICATE KEY UPDATE\n")
sql.write(" `Text_lang`=IF(VALUES(`Text_lang`)<>'',VALUES(`Text_lang`),`Text_lang`),\n")
sql.write(" `Text1_lang`=IF(VALUES(`Text1_lang`)<>'',VALUES(`Text1_lang`),`Text1_lang`),\n")
sql.write(" `VerifiedBuild`=VALUES(`VerifiedBuild`);\n")
sql.write("\nDROP TEMPORARY TABLE IF EXISTS `tmp_ruru_broadcast`;\n")
sql.write("CREATE TEMPORARY TABLE `tmp_ruru_broadcast` "
"(`id` INT PRIMARY KEY, `text_ru` TEXT) CHARACTER SET utf8mb4;\n")
sql.write("INSERT INTO `tmp_ruru_broadcast` (`id`,`text_ru`) VALUES\n")
sql.write(",\n".join(
f"({entry},{sql_quote(text)})" for entry, text in sorted(dynamic_rows.items())
))
sql.write(";\n\nINSERT INTO `broadcast_text_locale` "
"(`ID`,`locale`,`Text_lang`,`Text1_lang`,`VerifiedBuild`)\n")
sql.write("SELECT r.`id`,'ruRU',\n"
" CASE WHEN b.`Text` IS NOT NULL AND b.`Text`<>'' THEN r.`text_ru` ELSE '' END,\n"
" CASE WHEN b.`Text1` IS NOT NULL AND b.`Text1`<>'' THEN r.`text_ru` ELSE '' END,0\n"
"FROM `tmp_ruru_broadcast` r JOIN `broadcast_text` b ON b.`ID`=r.`id`\n"
"ON DUPLICATE KEY UPDATE\n"
" `Text_lang`=IF(VALUES(`Text_lang`)<>'',VALUES(`Text_lang`),`Text_lang`),\n"
" `Text1_lang`=IF(VALUES(`Text1_lang`)<>'',VALUES(`Text1_lang`),`Text1_lang`),\n"
" `VerifiedBuild`=VALUES(`VerifiedBuild`);\n"
"DROP TEMPORARY TABLE `tmp_ruru_broadcast`;\n")
return len(rows) + len(dynamic_rows)
def build_world(translated: dict[str, str]) -> int:
rows: dict[tuple[int, int, int], str] = {}
source = ROOT / "sql/sylvania/mardum_fr_creature_text.sql"
for line in source.read_text(encoding="utf-8").splitlines():
match = CREATURE_ROW.match(line)
if not match or match.group(4) not in translated:
continue
creature, group, entry, french = match.groups()
rows[(int(creature), int(group), int(entry))] = translated[french]
output = OUT_DIR / "ruRU_from_french_world.sql"
with output.open("w", encoding="utf-8", newline="\n") as sql:
sql.write("-- Generated from ChatGPT WoW localisation source.csv.\n")
sql.write("-- Apply to dc_world. Existing frFR rows are not modified.\n\n")
sql.write("SET NAMES utf8mb4;\n\n")
sql.write("INSERT INTO `creature_text_locale` "
"(`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`) VALUES\n")
values = []
for (creature, group, entry), text in sorted(rows.items()):
values.append(
f"({creature},{group},{entry},'ruRU',{sql_quote(text)})"
)
sql.write(",\n".join(values))
sql.write("\nON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);\n\n")
# These two source rows are assembled dynamically in the French module.
replacements = [
(97058, 1, 0, [("begins to cast", "начинает произносить"),
("[Carrion Storm]", translated.get("[Tempête de charogne]", "[Буря падали]"))]),
(97059, 2, 0, [("Nearby", "Ближайшие"),
("[Spider Eggs]", translated.get("[Œufs d’araignée]", "[Паучьи яйца]")),
("will hatch soon if not destroyed!.", "скоро вылупятся, если их не уничтожить!")]),
]
for creature, group, entry, pairs in replacements:
expression = "ct.`Text`"
for old, new in pairs:
expression = f"REPLACE({expression}, {sql_quote(old)}, {sql_quote(new)})"
sql.write(
"INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`)\n"
f"SELECT ct.`CreatureID`,ct.`GroupID`,ct.`ID`,'ruRU',{expression}\n"
f"FROM `creature_text` ct WHERE ct.`CreatureID`={creature} "
f"AND ct.`GroupID`={group} AND ct.`ID`={entry}\n"
"ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);\n\n"
)
gossip_fr = (
"Whitemoon, je vais vous emprunter une de vos chauves-souris gangrenées. "
"Nous allons chercher ce pour quoi nous sommes venus."
)
if gossip_fr in translated:
sql.write(
"INSERT INTO `gossip_menu_option_locale` "
"(`MenuId`,`OptionIndex`,`Locale`,`OptionText`,`BoxText`) VALUES\n"
f"(18776,0,'ruRU',{sql_quote(translated[gossip_fr])},'')\n"
"ON DUPLICATE KEY UPDATE `OptionText`=VALUES(`OptionText`);\n"
)
return len(rows) + 3
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
translated = translations()
hotfixes = build_hotfixes(translated)
world = build_world(translated)
print(f"hotfixes rows: {hotfixes}")
print(f"world rows: {world}")
if __name__ == "__main__":
main()
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Build a reviewable TSV inventory of likely French first-party strings."""
from __future__ import annotations
import re
import subprocess
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "reports" / "french_strings.tsv"
UNIQUE_OUTPUT = ROOT / "reports" / "french_strings_unique.tsv"
RUNTIME_OUTPUT = ROOT / "reports" / "french_runtime_strings_unique.tsv"
FIRST_PARTY = ("src/", "sql/", "sylvania/", "chantiers/", "docker/", "contrib/")
TEXT_SUFFIXES = {
".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".inl",
".sql", ".md", ".txt", ".conf", ".dist", ".sh", ".bat",
}
ACCENTS = re.compile(r"[àâçéèêëîïôùûüÿœæÀÂÇÉÈÊËÎÏÔÙÛÜŸŒÆ]")
WORDS = re.compile(
r"\b(?:alors|aucun|avec|avoir|besoin|cette|ceux|comme|comment|contre|"
r"dans|depuis|désormais|doit|donc|encore|entre|êtes|faire|faut|"
r"jusqu|leur|leurs|mais|merci|notre|pour|pourquoi|quand|sans|sera|"
r"sont|tout|tous|très|une|votre|vous|voici)\b",
re.IGNORECASE,
)
CONTRACTIONS = re.compile(r"\b(?:c|d|j|l|m|n|qu|s|t)'", re.IGNORECASE)
DOUBLE_QUOTED = re.compile(r'"(?:\\.|[^"\\])*"')
SINGLE_QUOTED = re.compile(r"'(?:\\.|''|[^'\\])*'")
def tracked_files() -> list[Path]:
output = subprocess.check_output(
["git", "ls-files", "-z"], cwd=ROOT
).decode("utf-8", "surrogateescape")
result = []
for name in output.split("\0"):
if not name or not name.startswith(FIRST_PARTY):
continue
path = ROOT / name
if path.suffix.lower() in TEXT_SUFFIXES:
result.append(path)
return result
def likely_french(text: str, path: Path) -> bool:
if "�" in text or len(text.strip()) < 2:
return False
accents = len(ACCENTS.findall(text))
words = len(WORDS.findall(text))
contractions = len(CONTRACTIONS.findall(text))
french_locale_file = bool(
re.search(r"(?:frfr|_fr(?:\.|_)|french)", path.as_posix(), re.IGNORECASE)
)
if accents:
return True
if words >= 2 or (words >= 1 and contractions >= 1):
return True
return french_locale_file and (" " in text or len(text) >= 8)
def clean_literal(value: str) -> str:
value = value[1:-1]
return value.replace("\t", "\\t").replace("\r", "\\r").replace("\n", "\\n")
def main() -> None:
rows: list[tuple[str, int, str, str]] = []
seen: set[tuple[str, int, str]] = set()
for path in tracked_files():
relative = path.relative_to(ROOT).as_posix()
try:
lines = path.read_text(encoding="utf-8").splitlines()
except (UnicodeDecodeError, OSError):
continue
for line_number, line in enumerate(lines, 1):
patterns = [DOUBLE_QUOTED]
if path.suffix.lower() == ".sql":
patterns.append(SINGLE_QUOTED)
found_literal = False
for pattern in patterns:
for match in pattern.finditer(line):
text = clean_literal(match.group(0)).strip()
if likely_french(text, path):
key = (relative, line_number, text)
if key not in seen:
rows.append((relative, line_number, "literal", text))
seen.add(key)
found_literal = True
# Documentation and comments may not use quoted literals.
stripped = line.strip()
is_comment_or_doc = (
path.suffix.lower() in {".md", ".txt"}
or stripped.startswith(("//", "#", "--", "/*", "*"))
)
if is_comment_or_doc and not found_literal and likely_french(stripped, path):
text = stripped.replace("\t", "\\t")
key = (relative, line_number, text)
if key not in seen:
rows.append((relative, line_number, "comment/doc", text))
seen.add(key)
rows.sort(key=lambda row: (row[0], row[1], row[2], row[3]))
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
with OUTPUT.open("w", encoding="utf-8", newline="\n") as report:
report.write("file\tline\tkind\tfrench_text\n")
for relative, line_number, kind, text in rows:
report.write(f"{relative}\t{line_number}\t{kind}\t{text}\n")
occurrences: dict[tuple[str, str], list[str]] = {}
for relative, line_number, kind, text in rows:
occurrences.setdefault((kind, text), []).append(f"{relative}:{line_number}")
with UNIQUE_OUTPUT.open("w", encoding="utf-8", newline="\n") as report:
report.write("kind\toccurrences\tfrench_text\tlocations\n")
for (kind, text), locations in sorted(
occurrences.items(), key=lambda item: (item[0][0], item[0][1])
):
report.write(
f"{kind}\t{len(locations)}\t{text}\t{' | '.join(locations)}\n"
)
runtime_occurrences: dict[str, list[str]] = {}
for relative, line_number, kind, text in rows:
if (
kind == "literal"
and relative.startswith(("src/", "sql/"))
and not relative.startswith("sql/test/")
):
runtime_occurrences.setdefault(text, []).append(f"{relative}:{line_number}")
with RUNTIME_OUTPUT.open("w", encoding="utf-8", newline="\n") as report:
report.write("occurrences\tfrench_text\trussian_text\tlocations\n")
for text, locations in sorted(runtime_occurrences.items()):
report.write(
f"{len(locations)}\t{text}\t\t{' | '.join(locations)}\n"
)
per_kind = Counter(row[2] for row in rows)
print(f"Report: {OUTPUT.relative_to(ROOT)}")
print(f"Total: {len(rows)}")
print(f"Unique: {len(occurrences)}")
print(f"Unique runtime literals: {len(runtime_occurrences)}")
for kind, count in sorted(per_kind.items()):
print(f"{kind}: {count}")
if __name__ == "__main__":
main()