Compare commits
12 Commits
20a8775eb3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 65f3535382 | |||
| 7657dd4731 | |||
| 25d255ddad | |||
| 2b6e2f12fb | |||
| 0c87f040f6 | |||
| 08426fa6d4 | |||
| 0a277174bc | |||
| 6956edf6cd | |||
| edb998de02 | |||
| 4d760528cc | |||
| a0464d3388 | |||
| c2f72294d8 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;'
|
||||
|
||||
@@ -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`);
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 запущен.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user