2 Commits

Author SHA1 Message Date
sindoring f262839c7c Merge branch 'encounter-journal' into release 2026-06-13 15:15:20 +04:00
sindoring df180281e6 Merge branch 'encounter-journal' into release 2026-06-12 18:40:46 +04:00
185 changed files with 726 additions and 15888 deletions
-47
View File
@@ -65,42 +65,6 @@ AC_RATE_XP_KILL=1
# Legacy alias для docker override. Держим синхронно с ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN.
AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN=0
# ------------------------------------------------------------------------------
# Playerbots AI / Yandex AI Studio
# ------------------------------------------------------------------------------
# Включайте после заполнения YANDEX_AI_API_KEY и YANDEX_AI_FOLDER_ID.
ACORE_PLAYERBOTS_AI_ENABLED=0
# Секретный API-ключ сервисного аккаунта Yandex AI Studio.
# Не коммитьте заполненное значение.
YANDEX_AI_API_KEY=
# Идентификатор каталога Yandex Cloud.
YANDEX_AI_FOLDER_ID=
# Пусто = gpt://<YANDEX_AI_FOLDER_ID>/aliceai-llm-flash
YANDEX_AI_MODEL_URI=
YANDEX_AI_BASE_URL=https://ai.api.cloud.yandex.net/v1
# Параметры генерации и игрового rate limit.
PLAYERBOTS_AI_REQUEST_TIMEOUT=12
PLAYERBOTS_AI_TEMPERATURE=0.4
PLAYERBOTS_AI_MAX_TOKENS=120
PLAYERBOTS_AI_HISTORY_MESSAGES=6
PLAYERBOTS_AI_LOG_API_RESPONSES=1
# Постоянный кэш классификации команд. 604800 секунд = 7 дней.
PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED=1
PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS=604800
PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES=10000
ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED=1
# 1 = доступно всем; 0 = недоступно никому до реализации подписки.
ACORE_PLAYERBOTS_AI_AVAILABLE_FOR_ALL=1
ACORE_PLAYERBOTS_AI_PLAYER_COOLDOWN_SECONDS=8
ACORE_PLAYERBOTS_AI_MAX_QUEUED_REQUESTS=32
ACORE_PLAYERBOTS_AI_MAX_REPLY_CHARACTERS=240
ACORE_PLAYERBOTS_AI_DEBUG=0
# ------------------------------------------------------------------------------
# Значения по умолчанию для ./create-account.sh
# ------------------------------------------------------------------------------
@@ -429,14 +393,6 @@ ACORE_LEARNSPELLS_RIDING_COLD_WEATHER_FLYING=1
# 1 = включает playerbots.conf, если модуль поддерживается сборкой.
ACORE_PLAYERBOTS_ENABLED=1
# 1 = при ./start-server.sh и scripts/prod-deploy.sh автоматически
# русифицировать имена RNDbot, названия их гильдий и статические сообщения.
# Ручной повторный запуск: ./localize-playerbots-ru.sh --apply
ACORE_PLAYERBOTS_RUSSIAN_NAMES=1
# 1 = разрешить ботам отвечать и участвовать в статическом чате.
ACORE_PLAYERBOTS_RANDOM_BOT_TALK=1
# Автологин random bots.
# Для сервера под соло/кооп лучше оставить 0, если вам нужны только боты в группу по команде.
ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN=0
@@ -456,9 +412,6 @@ ACORE_PLAYERBOTS_RANDOM_BOT_ACCOUNT_COUNT=0
# После этого ОБЯЗАТЕЛЬНО верните 0.
ACORE_PLAYERBOTS_DELETE_RANDOM_BOT_ACCOUNTS=0
# Разовая очистка гильдий randombot перед удалением их аккаунтов.
ACORE_PLAYERBOTS_DELETE_RANDOM_BOT_GUILDS=0
# Сколько ботов игрок может одновременно добавить в группу.
ACORE_PLAYERBOTS_MAX_ADDED_BOTS=9
-1
View File
@@ -108,4 +108,3 @@ local.properties
# !modules/yourmodule
#
# ==================
.claude
-39
View File
@@ -1,39 +0,0 @@
# Solo Random Dungeon client patch
The server recognizes five custom `LFGDungeons.dbc` entries:
| Custom ID | Stock category |
| --- | --- |
| 1258 | Random Classic Dungeon (258) |
| 1259 | Random Burning Crusade Dungeon (259) |
| 1260 | Random Burning Crusade Heroic (260) |
| 1261 | Random Lich King Dungeon (261) |
| 1262 | Random Lich King Heroic (262) |
The generated server DBC is at:
`client_patch/enUS/DBFilesClient/LFGDungeons.dbc`
It is mounted into `ac-worldserver` by `docker-compose.override.yml`.
## Client installation
Use the DBC extracted from the exact locale of the target 3.3.5a client:
```bash
./tools/build-solo-lfg-dbc.py \
/path/to/client/DBFilesClient/LFGDungeons.dbc \
/tmp/solo-rdf/DBFilesClient/LFGDungeons.dbc
```
Put the generated `DBFilesClient/LFGDungeons.dbc` into a client MPQ patch
loaded after the stock locale archives. For a Russian client this is normally
a new archive under `Data/ruRU/` named like `patch-ruRU-S.MPQ`.
After installing the patch, delete the client's `Cache` directory. The
Dungeon Finder list will contain separate solo random categories appropriate
for the character's level.
Do not replace a localized client DBC with the bundled enUS server DBC. Run
the generator against that client's own DBC so all stock localized rows remain
intact.
Binary file not shown.
@@ -1,45 +0,0 @@
-- MySQL dump 10.13 Distrib 8.4.3, for Win64 (x86_64)
--
-- Host: localhost Database: acore_characters
-- ------------------------------------------------------
-- Server version 8.4.3
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `mod_gamemodes_characters`
--
DROP TABLE IF EXISTS `mod_gamemodes_characters`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `mod_gamemodes_characters` (
`guid` int unsigned NOT NULL,
`game_mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0 = NORMAL, 1 = TRAITOR',
PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Character Game Modes';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `mod_gamemodes_characters`
--
LOCK TABLES `mod_gamemodes_characters` WRITE;
/*!40000 ALTER TABLE `mod_gamemodes_characters` DISABLE KEYS */;
/*!40000 ALTER TABLE `mod_gamemodes_characters` ENABLE KEYS */;
UNLOCK TABLES;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-169
View File
@@ -1,169 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}"
GUILD_NAME=""
ASSUME_YES=0
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
usage() {
cat <<EOF
Usage: $(basename "$0") <guild name> [--yes]
Permanently deletes a guild and its related character-database records.
The worldserver must be stopped before running this script.
Options:
--yes Skip the interactive confirmation
-h, --help Show this help
Examples:
docker compose stop ac-worldserver
./delete-guild.sh "Название гильдии"
./delete-guild.sh "Название гильдии" --yes
docker compose start ac-worldserver
EOF
}
log() {
printf '[delete-guild] %s\n' "$*"
}
while (($#)); do
case "$1" in
--yes)
ASSUME_YES=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
printf 'Unknown option: %s\n\n' "$1" >&2
usage >&2
exit 1
;;
*)
if [[ -n "$GUILD_NAME" ]]; then
printf 'Only one guild name may be specified\n\n' >&2
usage >&2
exit 1
fi
GUILD_NAME="$1"
shift
;;
esac
done
if [[ -z "$GUILD_NAME" ]]; then
usage >&2
exit 1
fi
if [[ "$GUILD_NAME" == *$'\n'* || "$GUILD_NAME" == *$'\r'* || "$GUILD_NAME" == *$'\t'* ]]; then
printf 'Guild name must not contain control characters\n' >&2
exit 1
fi
cd "$ROOT_DIR"
RUNNING_SERVICES="$(docker compose ps --status running --services)"
if grep -qx 'ac-worldserver' <<<"$RUNNING_SERVICES"; then
printf 'ac-worldserver is running. Stop it first:\n docker compose stop ac-worldserver\n' >&2
exit 1
fi
if ! grep -qx 'ac-database' <<<"$RUNNING_SERVICES"; then
printf 'ac-database is not running. Start it first:\n docker compose up -d ac-database\n' >&2
exit 1
fi
export ACORE_DELETE_GUILD_NAME="$GUILD_NAME"
SQL_GUILD_NAME="$(python3 <<'PY'
import os
value = os.environ["ACORE_DELETE_GUILD_NAME"]
print("'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'")
PY
)"
GUILD_ROW="$(
docker compose exec -T ac-database bash -lc \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --batch --skip-column-names acore_characters' \
<<<"SELECT guildid, name, leaderguid FROM guild WHERE name = ${SQL_GUILD_NAME} LIMIT 1;"
)"
if [[ -z "$GUILD_ROW" ]]; then
printf 'Guild not found: %s\n' "$GUILD_NAME" >&2
exit 1
fi
IFS=$'\t' read -r GUILD_ID FOUND_NAME LEADER_GUID <<<"$GUILD_ROW"
log "found guild id=${GUILD_ID}, name=${FOUND_NAME}, leader=${LEADER_GUID}"
if ((ASSUME_YES == 0)); then
printf 'This permanently deletes the guild and all items in its bank.\n'
printf 'Type the guild name to confirm: '
read -r CONFIRMATION
if [[ "$CONFIRMATION" != "$FOUND_NAME" ]]; then
printf 'Confirmation did not match; nothing was deleted.\n' >&2
exit 1
fi
fi
SQL=$(cat <<EOF
START TRANSACTION;
SET @guild_id := ${GUILD_ID};
CREATE TEMPORARY TABLE delete_guild_members (
guid INT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE=MEMORY
SELECT guid FROM guild_member WHERE guildid = @guild_id;
CREATE TEMPORARY TABLE delete_guild_bank_items (
item_guid INT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE=MEMORY
SELECT item_guid FROM guild_bank_item WHERE guildid = @guild_id;
DELETE item_instance
FROM item_instance
INNER JOIN delete_guild_bank_items
ON delete_guild_bank_items.item_guid = item_instance.guid;
DELETE guild_member_withdraw
FROM guild_member_withdraw
INNER JOIN delete_guild_members
ON delete_guild_members.guid = guild_member_withdraw.guid;
DELETE FROM guild_bank_eventlog WHERE guildid = @guild_id;
DELETE FROM guild_eventlog WHERE guildid = @guild_id;
DELETE FROM guild_bank_right WHERE guildid = @guild_id;
DELETE FROM guild_bank_item WHERE guildid = @guild_id;
DELETE FROM guild_bank_tab WHERE guildid = @guild_id;
DELETE FROM guild_rank WHERE guildid = @guild_id;
DELETE FROM guild_member WHERE guildid = @guild_id;
DELETE FROM guild WHERE guildid = @guild_id;
SET @deleted := ROW_COUNT();
COMMIT;
SELECT @guild_id AS guildid, @deleted AS deleted;
EOF
)
log "deleting guild ${FOUND_NAME} (${GUILD_ID})"
docker compose exec -T ac-database bash -lc \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --show-warnings acore_characters' <<<"$SQL"
log "guild deleted"
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +1,4 @@
services:
playerbots-ai:
container_name: playerbots-ai
image: moonwell/playerbots-ai:local
build:
context: ./services/playerbots-ai
dockerfile: Dockerfile
networks:
- ac-network
restart: unless-stopped
environment:
PLAYERBOTS_AI_PORT: "8080"
PLAYERBOTS_AI_REQUEST_TIMEOUT: "${PLAYERBOTS_AI_REQUEST_TIMEOUT:-12}"
PLAYERBOTS_AI_TEMPERATURE: "${PLAYERBOTS_AI_TEMPERATURE:-0.4}"
PLAYERBOTS_AI_MAX_TOKENS: "${PLAYERBOTS_AI_MAX_TOKENS:-120}"
PLAYERBOTS_AI_MAX_REPLY_CHARACTERS: "${ACORE_PLAYERBOTS_AI_MAX_REPLY_CHARACTERS:-240}"
PLAYERBOTS_AI_HISTORY_MESSAGES: "${PLAYERBOTS_AI_HISTORY_MESSAGES:-6}"
PLAYERBOTS_AI_COMMANDS_ENABLED: "${ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED:-1}"
PLAYERBOTS_AI_LOG_API_RESPONSES: "${PLAYERBOTS_AI_LOG_API_RESPONSES:-1}"
PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED: "${PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED:-1}"
PLAYERBOTS_AI_CLASSIFIER_CACHE_DB: "/data/classifier-cache.sqlite3"
PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS: "${PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS:-604800}"
PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES: "${PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES:-10000}"
PLAYERBOTS_AI_SYSTEM_PROMPT: "${PLAYERBOTS_AI_SYSTEM_PROMPT:-}"
YANDEX_AI_API_KEY: "${YANDEX_AI_API_KEY:-}"
YANDEX_AI_FOLDER_ID: "${YANDEX_AI_FOLDER_ID:-}"
YANDEX_AI_MODEL_URI: "${YANDEX_AI_MODEL_URI:-}"
YANDEX_AI_BASE_URL: "${YANDEX_AI_BASE_URL:-https://ai.api.cloud.yandex.net/v1}"
volumes:
- playerbots-ai-cache:/data
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=2)"
interval: 5s
timeout: 3s
retries: 12
ac-worldserver:
stdin_open: true
tty: true
@@ -47,7 +8,3 @@ services:
volumes:
- ./modules:/azerothcore/modules:ro
- ./lua_scripts:/azerothcore/lua_scripts
- ./client_patch/enUS/DBFilesClient/LFGDungeons.dbc:/azerothcore/env/dist/data/dbc/LFGDungeons.dbc:ro
volumes:
playerbots-ai-cache:
-12
View File
@@ -1,16 +1,4 @@
services:
playerbots-ai:
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "3"
ac-database:
command: >
--innodb_buffer_pool_size=4G
+1 -54
View File
@@ -15,7 +15,6 @@ fi
IMPORT_MYTHICPLUS_SQL="${ACORE_IMPORT_MYTHICPLUS_SQL:-1}"
IMPORT_STORE_SQL="${ACORE_IMPORT_STORE_SQL:-1}"
IMPORT_ENCOUNTER_JOURNAL_SQL="${ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL:-1}"
PLAYERBOTS_RUSSIAN_NAMES="${ACORE_PLAYERBOTS_RUSSIAN_NAMES:-1}"
usage() {
cat <<EOF
@@ -23,8 +22,7 @@ Usage: $(basename "$0")
Imports required custom SQL that lives outside the normal AzerothCore module paths.
Currently this bootstraps the MythicPlus and Store Lua packages, plus module strings
and playerbot localization data not covered by the normal module SQL import path
in this deployment.
that are not covered by the normal module SQL import path in this deployment.
EOF
}
@@ -191,28 +189,6 @@ import_store_sql() {
mysql_exec "" "CREATE DATABASE IF NOT EXISTS \`store\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
fi
# Unlike the rest of Store.sql, this is a repeatable migration. It ensures
# existing store databases receive the global switch without overwriting a
# value later changed by an administrator.
mysql_exec "store" "
CREATE TABLE IF NOT EXISTS \`store_config\` (
\`id\` TINYINT UNSIGNED NOT NULL,
\`enabled\` TINYINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT IGNORE INTO \`store_config\` (\`id\`, \`enabled\`) VALUES (1, 0);
"
# Store.sql is a one-time bootstrap, not a repeatable migration. It
# contains UPDATE and DELETE statements that would overwrite values
# maintained in the live store database. The canonical services table is
# created by the initial bootstrap, so its presence also protects
# installations created before this guard was introduced.
if database_exists "store" && table_exists "store" "store_services"; then
log "store database already initialized, skipping one-time Store.sql bootstrap"
return 0
fi
temp_sql="$(mktemp)"
{
@@ -259,32 +235,6 @@ import_aoe_loot_sql() {
import_sql_file "acore_world" "$aoe_loot_sql_file"
}
import_playerbots_ru_sql() {
local names_sql_file="$ROOT_DIR/modules/mod-playerbots/data/sql/characters/updates/2026_07_30_00_playerbots_russian_names.sql"
if [[ "$PLAYERBOTS_RUSSIAN_NAMES" == "0" ]]; then
log "Russian playerbot localization disabled, skipping name pools"
return 0
fi
if table_exists "acore_characters" "playerbots_names_ru" && \
table_exists "acore_characters" "playerbots_guild_names_ru"; then
log "Russian playerbot name pools already exist, skipping"
return 0
fi
import_sql_file "acore_characters" "$names_sql_file"
}
configure_server_motd() {
log "configuring MoonWell login message"
mysql_exec "acore_auth" "
REPLACE INTO motd (realmid, text)
VALUES (-1, 'Добро пожаловать на сервер MoonWell! Приятной игры!');
DELETE FROM motd_localized WHERE realmid = -1;
"
}
import_encounter_journal_sql() {
local schema_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/01_encounter_journal_schema.sql"
local sample_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/02_encounter_journal_sample.sql"
@@ -345,7 +295,6 @@ fi
require_integer_flag "ACORE_IMPORT_MYTHICPLUS_SQL" "$IMPORT_MYTHICPLUS_SQL"
require_integer_flag "ACORE_IMPORT_STORE_SQL" "$IMPORT_STORE_SQL"
require_integer_flag "ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL" "$IMPORT_ENCOUNTER_JOURNAL_SQL"
require_integer_flag "ACORE_PLAYERBOTS_RUSSIAN_NAMES" "$PLAYERBOTS_RUSSIAN_NAMES"
cd "$ROOT_DIR"
@@ -356,9 +305,7 @@ fi
import_mythicplus_sql
import_aoe_loot_sql
import_playerbots_ru_sql
import_encounter_journal_sql
import_store_sql
configure_server_motd
log "custom SQL bootstrap completed"
-213
View File
@@ -1,213 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
MODE="${1:---dry-run}"
CHARACTER_NAMES_SQL="modules/mod-playerbots/data/sql/characters/updates/2026_07_30_00_playerbots_russian_names.sql"
BOT_TEXTS_SQL="modules/mod-playerbots/data/sql/playerbots/updates/2026_07_30_00_ai_playerbot_russian_texts.sql"
case "$MODE" in
--dry-run | --apply) ;;
*)
echo "Usage: $0 [--dry-run|--apply]" >&2
exit 2
;;
esac
if ! docker compose ps --status running --services | grep -qx "ac-database"; then
echo "ac-database must be running." >&2
exit 1
fi
mysql_query() {
local database="$1"
local sql="$2"
docker compose exec -T ac-database bash -lc \
"mysql --default-character-set=utf8mb4 -N -s -uroot -p\"\$MYSQL_ROOT_PASSWORD\" \"$database\"" <<<"$sql"
}
import_sql() {
local database="$1"
local file_path="$2"
docker compose exec -T ac-database bash -lc \
"mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" \"$database\"" <"$file_path"
}
read -r bot_count guild_count < <(
mysql_query acore_characters "
SELECT
COUNT(DISTINCT CASE
WHEN REGEXP_LIKE(c.name, '^[[:ascii:]]+$') THEN c.guid
END),
COUNT(DISTINCT CASE
WHEN REGEXP_LIKE(g.name, '^[[:ascii:]]+$') THEN g.guildid
END)
FROM characters c
JOIN acore_playerbots.playerbots_account_type pat
ON pat.account_id = c.account AND pat.account_type = 1
LEFT JOIN guild g ON g.leaderguid = c.guid;
"
)
echo "RNDbot characters to rename: $bot_count"
echo "RNDbot-led guilds to rename: $guild_count"
if [[ "$MODE" == "--dry-run" ]]; then
echo "Dry run only. Stop ac-worldserver and run '$0 --apply' to apply."
exit 0
fi
if docker compose ps --status running --services | grep -qx "ac-worldserver"; then
echo "Refusing to rename live characters while ac-worldserver is running." >&2
echo "Stop it with: docker compose stop ac-worldserver" >&2
exit 1
fi
import_sql acore_characters "$CHARACTER_NAMES_SQL"
import_sql acore_playerbots "$BOT_TEXTS_SQL"
mysql_query acore_characters "
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS moonwell_playerbot_name_backup (
guid INT UNSIGNED NOT NULL,
original_name VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
localized_name VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (guid),
UNIQUE KEY uq_moonwell_playerbot_localized_name (localized_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS moonwell_playerbot_guild_name_backup (
guildid INT UNSIGNED NOT NULL,
original_name VARCHAR(24) NOT NULL,
localized_name VARCHAR(24) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (guildid),
UNIQUE KEY uq_moonwell_playerbot_localized_guild_name (localized_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_available_names;
CREATE TEMPORARY TABLE tmp_moonwell_available_names AS
SELECT
n.gender,
n.name,
ROW_NUMBER() OVER (PARTITION BY n.gender ORDER BY n.name_id) AS row_number_in_category
FROM playerbots_names_ru n
LEFT JOIN characters used
ON used.name = CONVERT(n.name USING utf8mb4) COLLATE utf8mb4_bin
LEFT JOIN moonwell_playerbot_name_backup reserved
ON reserved.localized_name = CONVERT(n.name USING utf8mb4) COLLATE utf8mb4_bin
WHERE used.guid IS NULL
AND reserved.guid IS NULL;
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_bot_targets;
CREATE TEMPORARY TABLE tmp_moonwell_bot_targets AS
SELECT
c.guid,
c.name AS original_name,
CASE
WHEN c.race IN (1, 5) THEN c.gender
WHEN c.race = 7 THEN 2 + c.gender
WHEN c.race = 3 THEN 4 + c.gender
WHEN c.race = 4 THEN 6 + c.gender
WHEN c.race = 11 THEN 8 + c.gender
WHEN c.race = 2 THEN 10 + c.gender
WHEN c.race = 8 THEN 12 + c.gender
WHEN c.race = 6 THEN 14 + c.gender
WHEN c.race = 10 THEN 16 + c.gender
END AS name_category,
ROW_NUMBER() OVER (
PARTITION BY CASE
WHEN c.race IN (1, 5) THEN c.gender
WHEN c.race = 7 THEN 2 + c.gender
WHEN c.race = 3 THEN 4 + c.gender
WHEN c.race = 4 THEN 6 + c.gender
WHEN c.race = 11 THEN 8 + c.gender
WHEN c.race = 2 THEN 10 + c.gender
WHEN c.race = 8 THEN 12 + c.gender
WHEN c.race = 6 THEN 14 + c.gender
WHEN c.race = 10 THEN 16 + c.gender
END
ORDER BY c.guid
) AS row_number_in_category
FROM characters c
JOIN acore_playerbots.playerbots_account_type pat
ON pat.account_id = c.account AND pat.account_type = 1
WHERE REGEXP_LIKE(c.name, '^[[:ascii:]]+$');
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_character_renames;
CREATE TEMPORARY TABLE tmp_moonwell_character_renames AS
SELECT t.guid, t.original_name, n.name AS localized_name
FROM tmp_moonwell_bot_targets t
JOIN tmp_moonwell_available_names n
ON n.gender = t.name_category
AND n.row_number_in_category = t.row_number_in_category;
START TRANSACTION;
INSERT IGNORE INTO moonwell_playerbot_name_backup (guid, original_name, localized_name)
SELECT guid, original_name, localized_name
FROM tmp_moonwell_character_renames;
UPDATE characters c
JOIN moonwell_playerbot_name_backup b ON b.guid = c.guid
SET c.name = b.localized_name
WHERE c.name = b.original_name;
DELETE declined
FROM character_declinedname declined
JOIN moonwell_playerbot_name_backup b ON b.guid = declined.guid;
COMMIT;
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_available_guild_names;
CREATE TEMPORARY TABLE tmp_moonwell_available_guild_names AS
SELECT
n.name,
ROW_NUMBER() OVER (ORDER BY n.name_id) AS row_number_in_pool
FROM playerbots_guild_names_ru n
LEFT JOIN guild used ON used.name = n.name
LEFT JOIN moonwell_playerbot_guild_name_backup reserved
ON reserved.localized_name = n.name
WHERE used.guildid IS NULL
AND reserved.guildid IS NULL;
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_guild_targets;
CREATE TEMPORARY TABLE tmp_moonwell_guild_targets AS
SELECT
g.guildid,
g.name AS original_name,
ROW_NUMBER() OVER (ORDER BY g.guildid) AS row_number_in_pool
FROM guild g
JOIN characters leader ON leader.guid = g.leaderguid
JOIN acore_playerbots.playerbots_account_type pat
ON pat.account_id = leader.account AND pat.account_type = 1
WHERE REGEXP_LIKE(g.name, '^[[:ascii:]]+$');
START TRANSACTION;
INSERT IGNORE INTO moonwell_playerbot_guild_name_backup (guildid, original_name, localized_name)
SELECT t.guildid, t.original_name, n.name
FROM tmp_moonwell_guild_targets t
JOIN tmp_moonwell_available_guild_names n
ON n.row_number_in_pool = t.row_number_in_pool;
UPDATE guild g
JOIN moonwell_playerbot_guild_name_backup b ON b.guildid = g.guildid
SET g.name = b.localized_name
WHERE g.name = b.original_name;
COMMIT;
"
read -r renamed_bot_count renamed_guild_count < <(
mysql_query acore_characters "
SELECT
(SELECT COUNT(*) FROM moonwell_playerbot_name_backup),
(SELECT COUNT(*) FROM moonwell_playerbot_guild_name_backup);
"
)
echo "Localized RNDbot characters: $renamed_bot_count"
echo "Localized RNDbot-led guilds: $renamed_guild_count"
echo "Static ruRU texts and Russian name pools are installed."
+13 -60
View File
@@ -5,7 +5,7 @@
local CONFIG = {
maxCategories = 11,
strings = {
categoryAccessDenied = "У вас нет доступа к этой категории!",
categoryAccessDenied = "You do not have access to this category!",
}
}
@@ -64,8 +64,7 @@ local KEYS = {
rewardCount_7 = 27,
rewardCount_8 = 28,
new = 29,
enabled = 30,
previewDisplayId = 31
enabled = 30
},
}
@@ -87,14 +86,7 @@ end
local StoreHandler = AIO.AddHandlers("STORE_CLIENT", {})
function StoreHandler.FrameData(player, storeEnabled, services, links, nav, currencies, rank)
SHOP_UI["Vars"].storeEnabled = storeEnabled == 1
SHOP_UI.StoreAvailability_Update()
if not SHOP_UI["Vars"].storeEnabled then
return
end
function StoreHandler.FrameData(player, services, links, nav, currencies, rank)
SHOP_UI["Data"].services = services
SHOP_UI["Data"].links = links
SHOP_UI["Data"].nav = nav
@@ -120,7 +112,6 @@ SHOP_UI = {
currentPage = 1,
maxPages = 1,
accountRank = 0,
storeEnabled = false,
["playerCurrencies"] = {}
},
["Data"] = {
@@ -153,13 +144,7 @@ function SHOP_UI.MainFrame_Create()
shopFrame.Title:SetFont("Fonts\\FRIZQT__.TTF", 14)
shopFrame.Title:SetShadowOffset(1, -1)
shopFrame.Title:SetPoint("TOP", shopFrame, "TOP", 0, -3)
shopFrame.Title:SetText("|cffedd100Лавка Лунной Жрицы|r")
shopFrame.UnavailableText = shopFrame:CreateFontString()
shopFrame.UnavailableText:SetFont("Fonts\\FRIZQT__.TTF", 24, "OUTLINE")
shopFrame.UnavailableText:SetShadowOffset(1, -1)
shopFrame.UnavailableText:SetPoint("CENTER", shopFrame, "CENTER", 85, 0)
shopFrame.UnavailableText:SetText("|cffedd100Извините, магазин ещё не работает|r")
shopFrame.Title:SetText("|cffedd100Shop|r")
-- create navigation button placeholders, pass parent as arg
SHOP_UI.NavButtons_Create(shopFrame)
@@ -203,7 +188,6 @@ function SHOP_UI.MainFrame_Create()
shopFrame:SetScript(
"OnShow",
function()
AIO.Handle("STORE_SERVER", "FrameData")
AIO.Handle("STORE_SERVER", "UpdateCurrencies")
PlaySound("AuctionWindowOpen", "Master")
end
@@ -222,32 +206,6 @@ function SHOP_UI.MainFrame_Create()
tinsert(UISpecialFrames, shopFrame:GetName())
SHOP_UI["FRAME"] = shopFrame
SHOP_UI.StoreAvailability_Update()
end
function SHOP_UI.StoreAvailability_Update()
local enabled = SHOP_UI["Vars"].storeEnabled
if SHOP_UI["FRAME"] and SHOP_UI["FRAME"].UnavailableText then
if enabled then
SHOP_UI["FRAME"].UnavailableText:Hide()
else
SHOP_UI["FRAME"].UnavailableText:Show()
end
end
for _, collectionName in pairs({"NAV_BUTTONS", "SERVICE_BUTTONS", "PAGING_ELEMENTS", "CURRENCY_BUTTONS"}) do
local collection = SHOP_UI[collectionName]
if collection and not enabled then
for _, element in pairs(collection) do
element:Hide()
end
end
end
if SHOP_UI["MODEL_FRAME"] and not enabled then
SHOP_UI["MODEL_FRAME"]:Hide()
end
end
-- create navigation button placeholders
@@ -375,9 +333,9 @@ function SHOP_UI.OnPurchaseConfirm(data)
end
StaticPopupDialogs["CONFIRM_STORE_PURCHASE"] = {
text = "Вы уверены, что хотите купить %s?",
button1 = "Да",
button2 = "Нет",
text = "Are you sure you want to purchase %s?",
button1 = "Yes",
button2 = "No",
OnAccept = function(self, data)
SHOP_UI.OnPurchaseConfirm(data)
end,
@@ -496,7 +454,7 @@ function SHOP_UI.ServiceBoxes_Create(parent)
service.buyButton.ButtonText = service.buyButton:CreateFontString()
service.buyButton.ButtonText:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
service.buyButton.ButtonText:SetPoint("CENTER", service.buyButton, 0, 0)
service.buyButton.ButtonText:SetText("Купить")
service.buyButton.ButtonText:SetText("Buy now!")
service.buyButton:SetScript(
"OnClick",
@@ -545,7 +503,7 @@ function SHOP_UI.ServiceBoxes_Create(parent)
if(self.Type == 1 and service.Flags == 1) then
SHOP_UI.ModelFrame_ShowPlayer(self.Rewards)
elseif((self.Type == 3 or self.Type == 4) and self.DisplayId > 0) then -- Handler for creatures with models
SHOP_UI.ModelFrame_ShowCreature(self.DisplayId, self.PreviewDisplayId)
SHOP_UI.ModelFrame_ShowCreature(self.DisplayId)
end
-- Parchment page sound
PlaySound(836)
@@ -656,7 +614,6 @@ function SHOP_UI.ServiceBoxes_Update()
service.Currency = serviceData[KEYS.service.currency]
service.TooltipHyperlink = serviceData[KEYS.service.hyperlink]
service.DisplayId = serviceData[KEYS.service.displayId]
service.PreviewDisplayId = serviceData[KEYS.service.previewDisplayId] or 0
service.Discount = serviceData[KEYS.service.discount]
service.Flags = serviceData[KEYS.service.flags]
service.New = serviceData[KEYS.service.new]
@@ -678,7 +635,7 @@ function SHOP_UI.ServiceBoxes_Update()
-- calculate discount percentage
local discountPct = math.floor(((service.Price - service.Discount) - service.Price) / service.Price * 100)
service.BannerText:SetFormattedText("|cffffffffСкидка: %i%%|r", discountPct)
service.BannerText:SetFormattedText("|cffffffffOn sale: %i%%|r", discountPct)
-- if service is discounted, then show all the discount frames and override the price text. otherwise hide.
if service.Discount > 1 then
@@ -1193,7 +1150,7 @@ function SHOP_UI.ModelFrame_ShowPlayer(displayId)
PlaySound("INTERFACESOUND_GAMESCROLLBUTTON", "Master")
end
function SHOP_UI.ModelFrame_ShowCreature(creatureEntry, previewDisplayId)
function SHOP_UI.ModelFrame_ShowCreature(displayId)
-- hacky ass model frame handling
-- hide model frames
if(SHOP_UI["MODEL_FRAME"].playerModel:IsShown()) then
@@ -1204,11 +1161,7 @@ function SHOP_UI.ModelFrame_ShowCreature(creatureEntry, previewDisplayId)
SHOP_UI["MODEL_FRAME"]:Hide()
-- set the correct unit and show frame
if(previewDisplayId and previewDisplayId > 0) then
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(creatureEntry, previewDisplayId)
else
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(creatureEntry)
end
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(displayId)
SHOP_UI["MODEL_FRAME"].creatureModel:Show()
SHOP_UI["MODEL_FRAME"]:Show()
@@ -1240,7 +1193,7 @@ local function ModifyGameMenuFrame()
storeButton.Text:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE")
storeButton.Text:SetShadowOffset(1, -1)
storeButton.Text:SetPoint("CENTER", storeButton, "CENTER", 0, 1)
storeButton.Text:SetText("|cffdbe005Лавка Лунной Жрицы");
storeButton.Text:SetText("|cffdbe005Store");
-- on click open the shop frame and hide the escape menu
storeButton:SetScript("OnClick", function()
+1 -25
View File
@@ -59,8 +59,7 @@ local KEYS = {
rewardCount_7 = 27,
rewardCount_8 = 28,
new = 29,
enabled = 30,
previewDisplayId = 31
enabled = 30
},
}
@@ -140,8 +139,6 @@ function ServiceData.Load()
Query:GetUInt32(KEYS.service.rewardCount_7),
Query:GetUInt32(KEYS.service.rewardCount_8),
Query:GetUInt32(KEYS.service.new),
Query:GetUInt32(KEYS.service.enabled),
0,
}
end
until not Query:NextRow()
@@ -227,27 +224,6 @@ function CreatureDisplays.Load()
until not ModelQuery:NextRow()
end
end
-- Keep the creature entry for the stock preview path, but also send its
-- primary CreatureDisplayInfo ID. WarcraftXL can apply that display
-- directly, avoiding stale or missing client creature-cache records.
for _, service in pairs(ServiceData.Cache) do
local serviceType = service[KEYS.service.serviceType]
local entry = service[KEYS.service.displayOrEntry]
if ((serviceType == 3 or serviceType == 4) and entry > 0) then
local creature = CreatureDisplays.Cache[entry]
local displayId = 0
if (creature) then
for modelIndex = 11, 14 do
if (creature[modelIndex] and creature[modelIndex] > 0) then
displayId = creature[modelIndex]
break
end
end
end
service[KEYS.service.previewDisplayId] = displayId
end
end
end
function LinkData.Load()
+62 -95
View File
@@ -7,16 +7,13 @@ local CONFIG = {
mailSenderGUID = 1, -- GUID of the character shown as sender of purchase mails
strings = {
-- Currency name is appended to the end of this string
insufficientFunds = "Недостаточно средств на балансе",
insufficientFunds = "You don't have enough",
-- Type is appended, ie. title, mount, pet etc.
alreadyKnownMount = "Этот транспорт уже изучен",
alreadyKnownPet = "Этот питомец уже изучен",
alreadyKnownTitle = "Этот титул уже получен",
tooHighLevel = "Уровень персонажа слишком высок",
mailBody = "Благодарим за покупку в Лавке Лунной Жрицы!",
alreadyKnown = "You already have this",
tooHighLevel = "Your level is too high",
mailBody = "Thank you for your purchase!",
-- The service name is prefixed to this message
successfulPurchase = "Покупка успешно совершена!",
storeUnavailable = "Магазин пока не работает"
successfulPurchase = "successfully purchased!"
}
}
@@ -25,24 +22,11 @@ local CONFIG = {
local AIO = AIO or require("AIO") and require("Store_DataStruct")
local CURRENCY_TYPES = {
[1] = "GOLD",
[2] = "ITEM_TOKEN",
[3] = "SERVER_HANDLED"
}
local purchaseReferenceSequence = 0
local function GetAccountBalance(player)
local query = AuthDBQuery(
"SELECT CAST(`balance` AS DOUBLE) FROM `account_balances` WHERE `account_id` = " ..
player:GetAccountId() .. " LIMIT 1;"
)
if not query then
return 0
end
return query:GetDouble(0)
end
local SHOP_UI = {
serviceHandlers = {
[1] = "ItemHandler", -- Okay
@@ -61,38 +45,27 @@ local KEYS = GetDataStructKeys();
local StoreHandler = AIO.AddHandlers("STORE_SERVER", {})
local function IsStoreEnabled()
local query = WorldDBQuery("SELECT `enabled` FROM `store`.`store_config` WHERE `id` = 1 LIMIT 1;")
return query and query:GetUInt32(0) == 1
end
function StoreHandler.FrameData(player)
local enabled = IsStoreEnabled()
AIO.Handle(
player,
"STORE_CLIENT",
"FrameData",
enabled and 1 or 0,
enabled and GetServiceData() or {},
enabled and GetLinkData() or {},
enabled and GetNavData() or {},
enabled and GetCurrencyData() or {},
player:GetGMRank()
)
AIO.Handle(player, "STORE_CLIENT", "FrameData", GetServiceData(), GetLinkData(), GetNavData(), GetCurrencyData(), player:GetGMRank())
end
function StoreHandler.UpdateCurrencies(player)
if not IsStoreEnabled() then
return
end
local tmp = {}
for currencyId, currency in pairs(GetCurrencyData()) do
local val = 0
local currencyTypeText = CURRENCY_TYPES[currency[KEYS.currency.currencyType]]
-- Handle the different currency types
if(currencyTypeText == "GOLD") then
val = math.floor(player:GetCoinage() / 10000)
end
if(currencyTypeText == "ITEM_TOKEN") then
val = player:GetItemCount(currency[KEYS.currency.data])
end
if(currencyTypeText == "SERVER_HANDLED") then
val = GetAccountBalance(player)
-- Add your custom handling here for retreiving your server handled currencies
end
-- If value is larger than 10k then truncate to make sure it fits within the shop frame
@@ -100,19 +73,12 @@ function StoreHandler.UpdateCurrencies(player)
val = "9999+"
end
-- Preserve the database currency id. A positional array silently breaks as
-- soon as ids have gaps or are reordered by MySQL.
tmp[currencyId] = val
table.insert(tmp, val)
end
AIO.Handle(player, "STORE_CLIENT", "UpdateCurrencies", tmp)
end
function StoreHandler.Purchase(player, serviceId)
if not IsStoreEnabled() then
player:SendAreaTriggerMessage(CONFIG.strings.storeUnavailable)
return
end
local services = GetServiceData()
-- See if the requested service exists
@@ -133,45 +99,46 @@ function StoreHandler.Purchase(player, serviceId)
player:PlayDirectSound(120, player)
-- Send success toast
player:SendAreaTriggerMessage(CONFIG.strings.successfulPurchase)
player:SendAreaTriggerMessage(services[serviceId][KEYS.service.name] .. " "..CONFIG.strings.successfulPurchase)
end
end
end
end
-- Helper functions
function SHOP_UI.DeductCurrency(player, currencyId, amount, serviceId)
function SHOP_UI.DeductCurrency(player, currencyId, amount)
local currency = GetCurrencyData()
if not currency[currencyId] then
return false
end
local currencyType = currency[currencyId][KEYS.currency.currencyType]
if CURRENCY_TYPES[currencyType] ~= "SERVER_HANDLED" then
local currencyName = currency[currencyId][KEYS.currency.name]
local currencyData = currency[currencyId][KEYS.currency.data]
-- Gold handling
if(CURRENCY_TYPES[currencyType] == "GOLD") then
if(player:GetCoinage() < amount * 10000) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.insufficientFunds.." "..currencyName.."|r")
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
return false
end
player:SetCoinage(player:GetCoinage() - (amount * 10000))
end
-- Token handling
if(CURRENCY_TYPES[currencyType] == "ITEM_TOKEN") then
if not(player:HasItem(currencyData, amount)) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.insufficientFunds.." "..currencyName.."|r")
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
return false
end
player:RemoveItem(currencyData, amount)
end
-- Other special handlingm you have to add your own integration here.
if(CURRENCY_TYPES[currencyType] == "SERVER_HANDLED") then
return false
end
purchaseReferenceSequence = purchaseReferenceSequence + 1
local reference = string.format(
"game-store:%u:%u:%u:%u:%u",
player:GetAccountId(),
player:GetGUIDLow(),
serviceId or 0,
os.time(),
purchaseReferenceSequence
)
local result = AuthDBQuery(
"SELECT `store_deduct_balance`(" .. player:GetAccountId() .. ", " .. amount .. ", '" .. reference .. "', " ..
player:GetGUIDLow() .. ", " .. (serviceId or 0) .. ") AS `balance_after`;"
)
local balanceAfter = result and result:GetDouble(0) or -1
if balanceAfter < 0 then
player:SendAreaTriggerMessage("|cFFFF0000" .. CONFIG.strings.insufficientFunds .. "|r")
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
return false
end
return true
end
@@ -185,7 +152,7 @@ function SHOP_UI.ItemHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -202,7 +169,7 @@ function SHOP_UI.ItemHandler(player, data)
end
-- Send reward mail
SendMail("Покупка: "..data[KEYS.service.name], CONFIG.strings.mailBody, player:GetGUIDLow(), CONFIG.mailSenderGUID, 62, 0, 0, 0, unpack(items))
SendMail("Purchase of: "..data[KEYS.service.name], CONFIG.strings.mailBody, player:GetGUIDLow(), CONFIG.mailSenderGUID, 62, 0, 0, 0, unpack(items))
return true
end
@@ -211,7 +178,7 @@ function SHOP_UI.GoldHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -239,13 +206,13 @@ function SHOP_UI.MountHandler(player, data)
-- check if player already has the spells learned
if(knownCount == rewardCount) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnownMount.."|r")
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." mount|r")
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
return false
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -277,13 +244,13 @@ function SHOP_UI.PetHandler(player, data)
-- check if player already has the spells learned
if(knownCount == rewardCount) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnownPet.."|r")
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." pet|r")
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
return false
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -304,7 +271,7 @@ function SHOP_UI.BuffHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -325,7 +292,7 @@ function SHOP_UI.ServiceHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -352,7 +319,7 @@ function SHOP_UI.LevelHandler(player, data)
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -381,13 +348,13 @@ function SHOP_UI.TitleHandler(player, data)
-- Check whether or not the player already has the specified title
if(player:HasTitle(data[KEYS.service.reward_1])) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnownTitle.."|r")
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." title|r")
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
return false
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
@@ -405,4 +372,4 @@ function SHOP_UI.UnusedHandler(player, data)
-- Since this is unused, always return false until the function is in use.
return false
end
end
+7 -2
View File
@@ -1,2 +1,7 @@
-- Login greeting is managed by the native AzerothCore MOTD in
-- import-custom-sql.sh. Keep this file as a harmless Eluna loading example.
local PLAYER_EVENT_ON_LOGIN = 3
local function OnLogin(event, player)
player:SendBroadcastMessage("Eluna greetings you")
end
RegisterPlayerEvent(PLAYER_EVENT_ON_LOGIN, OnLogin)
-7
View File
@@ -1,7 +0,0 @@
-- Game-mode UI synchronization is client-native since MoonWellClient 2.0.
--
-- The authoritative mode still comes from the server in SMSG_CHAR_ENUM as
-- CHARACTER_FLAG_UNK31 (0x40000000). WarcraftXL retains that bit while WoW
-- switches from GlueXML to the in-world FrameXML Lua state. Keeping this file
-- as a no-op avoids stale deployment scripts failing on a missing path while
-- removing the timing-sensitive AIO login event completely.
@@ -1207,9 +1207,6 @@ public:
void OnHeal(Unit* healer, Unit* receiver, uint32& gain) override
{
if (!healer)
return;
if (healer->IsPlayer())
sEluna->OnPlayerHeal(healer->ToPlayer(), receiver, gain);
@@ -1219,9 +1216,6 @@ public:
void OnDamage(Unit* attacker, Unit* receiver, uint32& damage) override
{
if (!attacker)
return;
if (attacker->IsPlayer())
sEluna->OnPlayerDamage(attacker->ToPlayer(), receiver, damage);
-1
View File
@@ -1 +0,0 @@
@@ -1,25 +0,0 @@
#
# Game Modes module configuration
#
########################################
# GameModes settings
########################################
#
# GameModes.Enable
# Description: Enable game modes system
# Default: 1 - (Enabled)
# 0 - (Disabled)
#
GameModes.Enable = 1
#
# GameModes.Traitor.Enable
# Description: Enable Traitor game mode
# Default: 1 - (Enabled)
# 0 - (Disabled)
#
GameModes.Traitor.Enable = 1
@@ -1,6 +0,0 @@
DROP TABLE IF EXISTS `mod_gamemodes_characters`;
CREATE TABLE `mod_gamemodes_characters` (
`guid` INT UNSIGNED NOT NULL,
`game_mode` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 = NORMAL, 1 = TRAITOR',
PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-1
View File
@@ -1 +0,0 @@
-69
View File
@@ -1,69 +0,0 @@
#include "GameModes.h"
#include "DatabaseEnv.h"
#include "Log.h"
GameModeMgr* GameModeMgr::Instance()
{
static GameModeMgr instance;
return &instance;
}
void GameModeMgr::LoadFromDB()
{
uint32 oldMSTime = getMSTime();
_gameModes.clear();
QueryResult result = CharacterDatabase.Query("SELECT `guid`, `game_mode` FROM `mod_gamemodes_characters`");
if (!result)
{
LOG_INFO("module", ">> Loaded 0 character game modes. Table `mod_gamemodes_characters` is empty.");
return;
}
uint32 count = 0;
do
{
Field* fields = result->Fetch();
ObjectGuid::LowType guid = fields[0].Get<uint32>();
uint8 mode = fields[1].Get<uint8>();
if (mode >= GAMEMODE_MAX)
{
LOG_ERROR("module", "mod-gamemodes: Invalid game mode {} for character {}, skipping.", mode, guid);
continue;
}
_gameModes[guid] = static_cast<GameMode>(mode);
++count;
} while (result->NextRow());
LOG_INFO("module", ">> Loaded {} character game modes in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
}
void GameModeMgr::SaveGameMode(ObjectGuid::LowType guid, GameMode mode)
{
_gameModes[guid] = mode;
CharacterDatabase.Execute("REPLACE INTO `mod_gamemodes_characters` (`guid`, `game_mode`) VALUES ({}, {})",
guid, static_cast<uint8>(mode));
}
void GameModeMgr::DeleteGameMode(ObjectGuid::LowType guid)
{
_gameModes.erase(guid);
CharacterDatabase.Execute("DELETE FROM `mod_gamemodes_characters` WHERE `guid` = {}", guid);
}
GameMode GameModeMgr::GetGameMode(ObjectGuid::LowType guid) const
{
auto it = _gameModes.find(guid);
if (it != _gameModes.end())
return it->second;
return GAMEMODE_NORMAL;
}
bool GameModeMgr::HasGameMode(ObjectGuid::LowType guid, GameMode mode) const
{
return GetGameMode(guid) == mode;
}
-37
View File
@@ -1,37 +0,0 @@
#ifndef MOD_GAMEMODES_H
#define MOD_GAMEMODES_H
#include "Player.h"
#include <unordered_map>
enum GameMode : uint8
{
GAMEMODE_NORMAL = 0,
GAMEMODE_TRAITOR = 1,
GAMEMODE_MAX
};
// Custom bit in SMSG_CHAR_ENUM character flags.
// Chosen as a high unused bit so the packet layout stays unchanged.
constexpr uint32 GAMEMODE_CHARACTER_FLAG_TRAITOR = 0x40000000;
class GameModeMgr
{
public:
static GameModeMgr* Instance();
void LoadFromDB();
void SaveGameMode(ObjectGuid::LowType guid, GameMode mode);
void DeleteGameMode(ObjectGuid::LowType guid);
GameMode GetGameMode(ObjectGuid::LowType guid) const;
bool HasGameMode(ObjectGuid::LowType guid, GameMode mode) const;
private:
GameModeMgr() = default;
std::unordered_map<ObjectGuid::LowType, GameMode> _gameModes;
};
#define sGameModeMgr GameModeMgr::Instance()
#endif // MOD_GAMEMODES_H
@@ -1,6 +0,0 @@
void AddSC_TraitorMode();
void Addmod_gamemodesScripts()
{
AddSC_TraitorMode();
}
@@ -1,291 +0,0 @@
#include "TraitorMode.h"
#include "GameModes.h"
#include "Config.h"
#include "DBCStores.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "ReputationMgr.h"
#include "ScriptMgr.h"
#include "WorldPacket.h"
#include "WorldSession.h"
namespace TraitorMode
{
namespace
{
void NormalizeFactionState(Player* player, FactionEntry const* factionEntry, bool visible, bool atWar)
{
if (!factionEntry || factionEntry->reputationListID < 0)
return;
FactionState* factionState = const_cast<FactionState*>(player->GetReputationMgr().GetState(factionEntry));
if (!factionState)
return;
factionState->Flags &= ~(FACTION_FLAG_VISIBLE | FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED |
FACTION_FLAG_PEACE_FORCED | FACTION_FLAG_INACTIVE);
if (visible)
factionState->Flags |= FACTION_FLAG_VISIBLE;
else
factionState->Flags |= FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED;
if (atWar)
factionState->Flags |= FACTION_FLAG_AT_WAR;
else
factionState->Flags &= ~FACTION_FLAG_AT_WAR;
factionState->needSend = true;
factionState->needSave = true;
}
void SetFactionStanding(Player* player, uint32 factionId, int32 targetDisplayed, bool visible, bool atWar)
{
FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId);
if (!factionEntry)
return;
// ReputationMgr::GetBaseReputation now always uses the player's actual race
// mask, so SetOneFactionReputation is safe to call directly.
player->GetReputationMgr().SetOneFactionReputation(factionEntry, static_cast<float>(targetDisplayed), false);
NormalizeFactionState(player, factionEntry, visible, atWar);
}
void NormalizeTraitorReputationStates(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE)
? HORDE_REPUTATION_FACTIONS
: ALLIANCE_REPUTATION_FACTIONS;
uint32 const* oldFactions = (originalTeam == TEAM_ALLIANCE)
? ALLIANCE_REPUTATION_FACTIONS
: HORDE_REPUTATION_FACTIONS;
auto normalizeFactionAndParent = [player](uint32 factionId, bool visible, bool atWar)
{
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
{
NormalizeFactionState(player, factionEntry, visible, atWar);
if (factionEntry->team)
NormalizeFactionState(player, sFactionStore.LookupEntry(factionEntry->team), visible, false);
}
};
for (uint32 i = 0; i < FACTION_COUNT; ++i)
{
normalizeFactionAndParent(newFactions[i], true, false);
normalizeFactionAndParent(oldFactions[i], false, true);
}
}
void SendFactionVisiblePacket(Player* player, FactionEntry const* factionEntry)
{
if (!factionEntry || factionEntry->reputationListID < 0)
return;
WorldPacket data(SMSG_SET_FACTION_VISIBLE, 4);
data << uint32(factionEntry->reputationListID);
player->SendDirectMessage(&data);
}
void SendTraitorFactionVisibility(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE)
? HORDE_REPUTATION_FACTIONS
: ALLIANCE_REPUTATION_FACTIONS;
auto sendFactionAndParent = [player](uint32 factionId)
{
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
{
SendFactionVisiblePacket(player, factionEntry);
if (factionEntry->team)
SendFactionVisiblePacket(player, sFactionStore.LookupEntry(factionEntry->team));
}
};
for (uint32 i = 0; i < FACTION_COUNT; ++i)
sendFactionAndParent(newFactions[i]);
}
}
void ApplyFactionSwap(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
TeamId newTeam = (originalTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE;
player->setTeamId(newTeam);
// Derive faction template from the canonical race for the new team (same
// logic as SetFactionForRace), so we never rely on hardcoded DBC IDs.
uint8 representativeRace = (newTeam == TEAM_HORDE) ? HORDE_DEFAULT_RACE : ALLIANCE_DEFAULT_RACE;
if (ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(representativeRace))
player->SetFaction(rEntry->FactionID);
LOG_INFO("module", "mod-gamemodes: ApplyFactionSwap for {} (GUID: {}), original team = {}, new team = {}, factionTemplate = {}",
player->GetName(), player->GetGUID().GetCounter(),
(originalTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"),
(newTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"),
player->GetFaction());
}
void InitTraitorReputations(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
// New faction reputations -> Neutral (standing = 0)
const uint32* newFactions = (originalTeam == TEAM_ALLIANCE)
? HORDE_REPUTATION_FACTIONS
: ALLIANCE_REPUTATION_FACTIONS;
for (uint32 i = 0; i < FACTION_COUNT; ++i)
SetFactionStanding(player, newFactions[i], 0, true, false);
// Old faction reputations -> Hostile (standing = -6000)
const uint32* oldFactions = (originalTeam == TEAM_ALLIANCE)
? ALLIANCE_REPUTATION_FACTIONS
: HORDE_REPUTATION_FACTIONS;
for (uint32 i = 0; i < FACTION_COUNT; ++i)
SetFactionStanding(player, oldFactions[i], -6000, false, true);
LOG_INFO("module", "mod-gamemodes: InitTraitorReputations for {} (GUID: {})",
player->GetName(), player->GetGUID().GetCounter());
}
void TeleportToNewFactionStart(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
uint8 spawnRace = (originalTeam == TEAM_ALLIANCE)
? HORDE_DEFAULT_RACE
: ALLIANCE_DEFAULT_RACE;
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(spawnRace, player->getClass());
if (!info)
{
// Fallback: try warrior (class 1) for that race
info = sObjectMgr->GetPlayerInfo(spawnRace, 1);
}
if (!info)
{
LOG_ERROR("module", "mod-gamemodes: No spawn info for race {} class {}", spawnRace, player->getClass());
return;
}
player->TeleportTo(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
LOG_INFO("module", "mod-gamemodes: Teleported {} to faction start (map {}, {:.1f}, {:.1f}, {:.1f})",
player->GetName(), info->mapId, info->positionX, info->positionY, info->positionZ);
}
}
class TraitorModePlayer : public PlayerScript
{
public:
TraitorModePlayer() : PlayerScript("TraitorModePlayer",
{PLAYERHOOK_ON_CREATE, PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, PLAYERHOOK_ON_UPDATE_FACTION}) {}
// OnPlayerCreate fires on a temporary Player object AFTER SaveToDB.
// Changes to faction/reputation here are lost. We only save the game mode to DB.
void OnPlayerCreate(Player* player) override
{
LOG_INFO("module", "mod-gamemodes: OnPlayerCreate for {} (GUID: {}), OutfitId = {}",
player->GetName(), player->GetGUID().GetCounter(), player->GetOutfitId());
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (player->GetOutfitId() != GAMEMODE_TRAITOR)
return;
sGameModeMgr->SaveGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR);
CharacterDatabase.Execute("UPDATE `characters` SET `cinematic` = 1 WHERE `guid` = {}", player->GetGUID().GetCounter());
LOG_INFO("module", "mod-gamemodes: Saved TRAITOR mode for {} (GUID: {})",
player->GetName(), player->GetGUID().GetCounter());
}
// Reputation load completes before initial packets are sent to the client.
// Traitor mode must rewrite faction/reputation state here, otherwise the client
// builds the reputation window from the original race defaults.
void OnPlayerReputationLoadFromDB(Player* player) override
{
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
return;
TraitorMode::ApplyFactionSwap(player);
if (player->HasAtLoginFlag(AT_LOGIN_FIRST))
TraitorMode::InitTraitorReputations(player);
TraitorMode::NormalizeTraitorReputationStates(player);
}
void OnPlayerLogin(Player* player) override
{
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
return;
TraitorMode::ApplyFactionSwap(player);
TraitorMode::NormalizeTraitorReputationStates(player);
player->GetReputationMgr().SendInitialReputations();
TraitorMode::SendTraitorFactionVisibility(player);
player->SetPlayerFlag(PLAYER_FLAGS_TRAITOR);
if (player->HasAtLoginFlag(AT_LOGIN_FIRST))
TraitorMode::TeleportToNewFactionStart(player);
LOG_INFO("module", "mod-gamemodes: OnPlayerLogin END for {} — TeamId = {}, FactionTemplate = {}, OriginalTeam = {}",
player->GetName(),
static_cast<uint32>(player->GetTeamId()),
player->GetFaction(),
static_cast<uint32>(player->GetTeamId(true)));
}
// OnPlayerUpdateFaction fires every time SetFactionForRace is called
// (login, race change, etc). Re-apply the swap so it's never lost.
void OnPlayerUpdateFaction(Player* player) override
{
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
return;
TraitorMode::ApplyFactionSwap(player);
}
};
class TraitorModeWorld : public WorldScript
{
public:
TraitorModeWorld() : WorldScript("TraitorModeWorld",
{WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE}) {}
void OnLoadCustomDatabaseTable() override
{
sGameModeMgr->LoadFromDB();
}
};
void AddSC_TraitorMode()
{
new TraitorModePlayer();
new TraitorModeWorld();
}
@@ -1,49 +0,0 @@
#ifndef MOD_GAMEMODES_TRAITOR_H
#define MOD_GAMEMODES_TRAITOR_H
#include "Player.h"
namespace TraitorMode
{
// Reputation faction IDs: capital cities + PvP battleground factions
constexpr uint32 ALLIANCE_REPUTATION_FACTIONS[] = {
72, // Stormwind
47, // Ironforge
69, // Darnassus
54, // Gnomeregan Exiles
930, // Exodar
890, // Silverwing Sentinels (WSG)
730, // Stormpike Guard (AV)
509, // League of Arathor (AB)
};
constexpr uint32 HORDE_REPUTATION_FACTIONS[] = {
76, // Orgrimmar
81, // Thunder Bluff
68, // Undercity
530, // Darkspear Trolls
911, // Silvermoon City
889, // Warsong Outriders (WSG)
729, // Frostwolf Clan (AV)
510, // The Defilers (AB)
};
constexpr uint32 FACTION_COUNT = 8;
// Default spawn points for each faction (race 2 = Orc for Horde, race 1 = Human for Alliance)
constexpr uint8 HORDE_DEFAULT_RACE = 2; // Orc
constexpr uint8 ALLIANCE_DEFAULT_RACE = 1; // Human
// Apply faction swap on login/faction update
void ApplyFactionSwap(Player* player);
// Set new faction reputations to Neutral (standing 0)
void InitTraitorReputations(Player* player);
// Teleport traitor to the opposite faction's starting zone
void TeleportToNewFactionStart(Player* player);
}
void AddSC_TraitorMode();
#endif // MOD_GAMEMODES_TRAITOR_H
@@ -47,6 +47,10 @@ public:
sIndividualProgression->CheckAdjustments(player);
if (sIndividualProgression->enabled)
{
ChatHandler(player->GetSession()).SendSysMessage("|cff00ff00Individual Progression: |cffccccccenabled|r");
}
}
void OnPlayerSetMaxLevel(Player* player, uint32& maxPlayerLevel) override
@@ -17,7 +17,6 @@
#include "QuestDef.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include "WorldSession.h"
#include <cstdlib>
#include <string>
@@ -32,8 +31,6 @@ namespace MoonWell::PlayerGuide
return;
if (!player || !player->GetSession() || player->IsBeingTeleported())
return;
if (player->GetSession()->IsBot())
return;
Payload p = sPlayerGuideMgr->BuildFor(player);
std::string json = sPlayerGuideMgr->SerializeJson(p);
@@ -25,29 +25,6 @@ namespace MoonWell::PlayerGuide
// The mgr clamps its configured chunk size into a safe range.
constexpr std::size_t HARD_LIMIT = 250;
std::size_t Utf8ChunkSize(std::string_view body, std::size_t offset,
std::size_t budget)
{
std::size_t take = std::min(budget, body.size() - offset);
if (offset + take == body.size())
return take;
// A chunk must not end between a UTF-8 leading byte and one of
// its continuation bytes. Chat packet validation rejects such
// an individually invalid string before the addon can reassemble
// the complete JSON payload.
while (take > 0 &&
(static_cast<unsigned char>(body[offset + take]) & 0xC0) == 0x80)
{
--take;
}
// The configured budget is clamped to at least 64 bytes, so a
// valid UTF-8 code point always fits. Keep a defensive fallback
// for malformed input to guarantee forward progress.
return take > 0 ? take : std::min(budget, body.size() - offset);
}
void SendOneChunk(Player* player, std::string_view command,
uint32 seq, uint32 total, std::string_view body)
{
@@ -94,20 +71,16 @@ namespace MoonWell::PlayerGuide
return;
}
// Calculate UTF-8-safe slices first because backing up from a
// continuation byte can increase the number of chunks.
std::vector<std::string_view> chunks;
for (std::size_t off = 0; off < body.size();)
// Multi-chunk: split blindly by byte budget. JSON tolerates
// concatenation on the client.
uint32 total = static_cast<uint32>(
(body.size() + budget - 1) / budget);
uint32 seq = 1;
for (std::size_t off = 0; off < body.size(); off += budget, ++seq)
{
std::size_t take = Utf8ChunkSize(body, off, budget);
chunks.emplace_back(body.data() + off, take);
off += take;
}
uint32 total = static_cast<uint32>(chunks.size());
for (uint32 i = 0; i < total; ++i)
{
SendOneChunk(player, command, i + 1, total, chunks[i]);
std::size_t take = std::min(budget, body.size() - off);
SendOneChunk(player, command, seq, total,
std::string_view(body.data() + off, take));
}
}
-114
View File
@@ -1,114 +0,0 @@
# MoonWell Playerbots AI
This module sends a local `/say` or public-channel message to the LLM only when
a real player mentions the complete name of an online bot. A channel bot must
be a channel member; a `/say` bot must be within the server's normal listening
range. HTTP work runs outside the AzerothCore world thread.
Alice AI LLM Flash can classify direct natural-language requests into a small
allowlisted set of native playerbots commands. The server validates the command
again and executes it only against the addressed bot; existing playerbots
ownership and group security checks still apply.
The routing prompt covers party movement and combat, LFG, combat/non-combat
strategies, spells, RTI/RTSC, focus healing, loot lists, inventory operations,
quests, pets, talents, glyphs, maintenance, and information commands documented
in the upstream Playerbot Commands wiki. Administrative, console, account,
debug/cheat, and destructive `destroy`/`drop`/mail commands are intentionally
not executable through the LLM.
Common Russian movement/combat phrases use a deterministic fast path. Other
requests first pass through a command-only classifier; ordinary conversation is
then sent separately to the roleplay prompt. While AI routing is enabled, the
legacy public-channel parser is bypassed and `setup-modules.sh` also forces
`AiPlayerbot.RandomBotTalk = 0`. RandomBotTalk consumes incoming chat packets
inside each bot, so bypassing only the parser does not prevent its delayed
stock replies. Whisper, party, raid, and guild command hooks remain unchanged.
Recognized commands produce a gender-aware local-chat acknowledgement
(`Понял команду.` or `Поняла команду.`) before they are handed to the native
playerbots command handler. Bot and player gender are also included in the LLM
context for pronoun, past-tense verb, and adjective agreement. Russian requests
to show bags or inventory use the native `inv` command; `open items` is reserved
for actually opening container items.
Trade and quest requests have deterministic link-preserving routes before the
LLM classifier:
- `дай/передай [item]` -> `t [item]`; `начни обмен` -> `t`
- `покажи задания` -> `quests all`; completed/incomplete filters are supported
- `покажи прогресс [quest]` -> `q [quest]`
- `прими [quest]` / `прими все задания` -> `accept [quest]` / `accept *`
- `поделись [quest]` -> `share [quest]`
- `выбери [item] в награду` -> `r [item]`
- `сдай готовое задание` -> `talk`
- `откажись от [quest]` -> `drop [quest]` only with an explicit quest link
These routes preserve the original WoW hyperlink byte-for-byte. The local
playerbots trade action continues processing after it creates the trade, so one
`t [item]` command both initiates the exchange and places the requested item in
the trade window.
The bundled `playerbots-ai` bridge uses the Yandex AI Studio OpenAI-compatible
Chat Completions API and defaults to Alice AI LLM Flash.
## Configuration
Set these values in the repository `.env`:
```dotenv
ACORE_PLAYERBOTS_AI_ENABLED=1
ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED=1
YANDEX_AI_API_KEY=<service-account API key>
YANDEX_AI_FOLDER_ID=<Yandex Cloud folder ID>
```
Until the subscription system exists, public access is controlled by one
explicit switch:
```dotenv
ACORE_PLAYERBOTS_AI_AVAILABLE_FOR_ALL=1
```
`1` allows every player to reach the bridge. `0` denies LLM access to everyone
without spending tokens. When subscriptions are implemented, their entitlement
check will be added to the disabled branch instead of using a temporary manual
account list.
The command classifier has a persistent SQLite cache in the
`playerbots-ai-cache` Docker volume. The key excludes the addressed bot name,
so repeated phrases such as “покажи инвентарь” are shared across players and
bots. Both commands and “ordinary chat, not a command” decisions are cached.
The cache does not store roleplay responses or conversation history.
Obvious social phrases such as greetings, “как дела?” and “расскажи о себе”
skip the command classifier entirely: they still receive an Alice roleplay
reply, but do not pay the roughly 800-token command-catalog prompt first.
```dotenv
PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED=1
PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS=604800
PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES=10000
```
Changing the classifier prompt automatically creates a new cache namespace.
Old rows expire normally, and the oldest rows are removed at the size limit.
The default model URI is:
```text
gpt://<YANDEX_AI_FOLDER_ID>/aliceai-llm-flash
```
Run `./start-server.sh` or `scripts/prod-deploy.sh`. The deployment wrapper
builds and starts the bridge before `ac-worldserver`.
Never commit a populated API key.
Full Yandex response JSON is logged by default by the bridge:
```dotenv
PLAYERBOTS_AI_LOG_API_RESPONSES=1
```
Use `docker compose logs -f playerbots-ai` to follow classifier and chat
responses. Authentication headers and the API key are never logged.
@@ -1,29 +0,0 @@
#
# MoonWell Playerbots AI
#
# Enables LLM replies in public channels.
PlayerbotsAI.Enabled = 0
# Enables natural-language commands through an allowlisted playerbots command set.
PlayerbotsAI.CommandsEnabled = 1
# Temporary public-access switch. A future subscription check will be inserted
# when this is disabled; until that system exists, 0 denies access to everyone.
PlayerbotsAI.AvailableForAll = 1
# Internal HTTP bridge. The Yandex API key is kept in the bridge container.
PlayerbotsAI.BridgeHost = playerbots-ai
PlayerbotsAI.BridgePort = 8080
PlayerbotsAI.BridgePath = /v1/chat
# Per-player request cooldown and bounded worker queue.
PlayerbotsAI.PlayerCooldownSeconds = 8
PlayerbotsAI.MaxQueuedRequests = 32
# Maximum UTF-8 characters sent back to an in-game channel.
PlayerbotsAI.MaxReplyCharacters = 240
# Extra request/response logging. Prompts and API keys are never logged.
PlayerbotsAI.Debug = 0
п
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
# This module uses AzerothCore's standard source and loader discovery.
@@ -1,808 +0,0 @@
#include "Channel.h"
#include "ChannelMgr.h"
#include "Config.h"
#include "Language.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h"
#include "World.h"
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/asio/write.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <iomanip>
#include <map>
#include <mutex>
#include <optional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <thread>
#include <utility>
#include <vector>
namespace
{
struct AiRequest
{
ObjectGuid botGuid;
ObjectGuid playerGuid;
TeamId teamId;
std::string channelName;
std::string host;
std::string port;
std::string path;
std::string body;
};
struct AiResponse
{
ObjectGuid botGuid;
ObjectGuid playerGuid;
TeamId teamId;
std::string channelName;
std::string command;
std::string text;
};
struct BridgeResponse
{
std::string command;
std::string text;
};
std::string JsonEscape(std::string const& value)
{
std::ostringstream output;
for (unsigned char character : value)
{
switch (character)
{
case '"': output << "\\\""; break;
case '\\': output << "\\\\"; break;
case '\b': output << "\\b"; break;
case '\f': output << "\\f"; break;
case '\n': output << "\\n"; break;
case '\r': output << "\\r"; break;
case '\t': output << "\\t"; break;
default:
if (character < 0x20)
{
output << "\\u" << std::hex << std::setw(4) << std::setfill('0')
<< static_cast<uint32>(character) << std::dec;
}
else
{
output << static_cast<char>(character);
}
break;
}
}
return output.str();
}
std::vector<uint32> DecodeAndCaseFold(std::string const& text)
{
std::vector<uint32> result;
result.reserve(text.size());
for (std::size_t index = 0; index < text.size();)
{
unsigned char first = static_cast<unsigned char>(text[index]);
uint32 codepoint = 0xFFFD;
std::size_t length = 1;
if (first < 0x80)
{
codepoint = first;
}
else if ((first & 0xE0) == 0xC0 && index + 1 < text.size())
{
codepoint = ((first & 0x1F) << 6)
| (static_cast<unsigned char>(text[index + 1]) & 0x3F);
length = 2;
}
else if ((first & 0xF0) == 0xE0 && index + 2 < text.size())
{
codepoint = ((first & 0x0F) << 12)
| ((static_cast<unsigned char>(text[index + 1]) & 0x3F) << 6)
| (static_cast<unsigned char>(text[index + 2]) & 0x3F);
length = 3;
}
else if ((first & 0xF8) == 0xF0 && index + 3 < text.size())
{
codepoint = ((first & 0x07) << 18)
| ((static_cast<unsigned char>(text[index + 1]) & 0x3F) << 12)
| ((static_cast<unsigned char>(text[index + 2]) & 0x3F) << 6)
| (static_cast<unsigned char>(text[index + 3]) & 0x3F);
length = 4;
}
if (codepoint >= 'A' && codepoint <= 'Z')
codepoint += 'a' - 'A';
else if (codepoint >= 0x0410 && codepoint <= 0x042F)
codepoint += 0x20;
else if (codepoint == 0x0401)
codepoint = 0x0451;
result.push_back(codepoint);
index += length;
}
return result;
}
bool IsWordCodepoint(uint32 codepoint)
{
return (codepoint >= '0' && codepoint <= '9')
|| (codepoint >= 'a' && codepoint <= 'z')
|| (codepoint >= 0x0430 && codepoint <= 0x044F)
|| codepoint == 0x0451;
}
std::optional<std::size_t> FindCompleteName(
std::vector<uint32> const& message,
std::vector<uint32> const& name)
{
if (name.empty() || name.size() > message.size())
return std::nullopt;
for (std::size_t position = 0; position + name.size() <= message.size(); ++position)
{
if (!std::equal(name.begin(), name.end(), message.begin() + position))
continue;
bool leftBoundary = position == 0 || !IsWordCodepoint(message[position - 1]);
std::size_t end = position + name.size();
bool rightBoundary = end == message.size() || !IsWordCodepoint(message[end]);
if (leftBoundary && rightBoundary)
return position;
}
return std::nullopt;
}
std::string NormalizeReply(std::string text, uint32 maxCharacters)
{
std::replace(text.begin(), text.end(), '\r', ' ');
std::replace(text.begin(), text.end(), '\n', ' ');
std::string compact;
compact.reserve(text.size());
bool previousSpace = false;
for (unsigned char character : text)
{
bool isSpace = character == ' ' || character == '\t';
if (isSpace && previousSpace)
continue;
compact.push_back(isSpace ? ' ' : static_cast<char>(character));
previousSpace = isSpace;
}
std::size_t first = compact.find_first_not_of(' ');
if (first == std::string::npos)
return {};
compact.erase(0, first);
std::size_t last = compact.find_last_not_of(' ');
compact.erase(last + 1);
if (!maxCharacters)
return compact;
std::size_t byteIndex = 0;
uint32 characters = 0;
while (byteIndex < compact.size() && characters < maxCharacters)
{
unsigned char firstByte = static_cast<unsigned char>(compact[byteIndex]);
std::size_t length = firstByte < 0x80 ? 1
: (firstByte & 0xE0) == 0xC0 ? 2
: (firstByte & 0xF0) == 0xE0 ? 3
: (firstByte & 0xF8) == 0xF0 ? 4
: 1;
byteIndex = std::min(compact.size(), byteIndex + length);
++characters;
}
if (byteIndex < compact.size())
{
compact.resize(byteIndex);
compact += "…";
}
return compact;
}
BridgeResponse ParseBridgeResponse(std::string response)
{
static std::string const commandPrefix = "PB-COMMAND:";
if (response.rfind(commandPrefix, 0) != 0)
return {"", std::move(response)};
std::size_t newline = response.find('\n');
if (newline == std::string::npos)
return {"", ""};
return {
response.substr(commandPrefix.size(), newline - commandPrefix.size()),
response.substr(newline + 1)
};
}
bool IsAllowedPlayerbotCommand(std::string const& command)
{
if (command.empty() || command.size() > 300 || command.find('\\') != std::string::npos)
return false;
if (std::any_of(command.begin(), command.end(), [](unsigned char character)
{
return character < 0x20;
}))
{
return false;
}
static std::set<std::string> const allowedCommands = {
"accept",
"accept *",
"add all loot",
"attack",
"attack rti target",
"autogear",
"buff",
"disperse disable",
"flee",
"focus heal ?",
"focus heal clear",
"focus heal none",
"focus heal unset",
"follow",
"give leader",
"glyphs",
"grind",
"help",
"home",
"inv",
"invite",
"items",
"leave",
"lfg",
"ll all",
"ll gray",
"ll normal",
"ll quest",
"ll skill",
"los",
"maintenance",
"open items",
"outfit ?",
"pet aggressive",
"pet attack",
"pet defensive",
"pet follow",
"pet passive",
"pet stance",
"pet stay",
"quests",
"quests all",
"quests completed",
"quests incompleted",
"ready",
"release",
"reset",
"reset botAI",
"revive",
"roll",
"rti",
"rtsc",
"rtsc cancel",
"rtsc go save",
"rtsc toggle",
"runaway",
"spells",
"ss reset",
"stay",
"stats",
"summon",
"t",
"talents",
"talents spec list",
"talk",
"tame",
"tame family",
"tank attack",
"trainer",
"trainer learn",
"who"
};
if (allowedCommands.find(command) != allowedCommands.end())
return true;
static std::vector<std::string> const allowedPrefixes = {
"accept ",
"b ",
"bank ",
"cast ",
"co ",
"disperse set ",
"e ",
"focus heal ",
"gb ",
"glyph equip ",
"lfg ",
"ll ",
"nc ",
"outfit ",
"pet ",
"q ",
"r ",
"roll ",
"rti ",
"rtsc ",
"s ",
"ss ",
"share ",
"talents apply ",
"talents spec ",
"tame ",
"t ",
"drop ",
"u ",
"ue ",
"who "
};
return std::any_of(
allowedPrefixes.begin(),
allowedPrefixes.end(),
[&command](std::string const& prefix)
{
return command.size() > prefix.size() && command.rfind(prefix, 0) == 0;
});
}
std::string ReadHttpBody(
std::string const& host,
std::string const& port,
std::string const& path,
std::string const& body)
{
using boost::asio::ip::tcp;
boost::asio::io_context ioContext;
tcp::resolver resolver(ioContext);
tcp::socket socket(ioContext);
boost::asio::connect(socket, resolver.resolve(host, port));
std::ostringstream request;
request << "POST " << path << " HTTP/1.1\r\n"
<< "Host: " << host << ':' << port << "\r\n"
<< "User-Agent: MoonWell-playerbots-ai/1.0\r\n"
<< "Content-Type: application/json; charset=utf-8\r\n"
<< "Accept: text/plain\r\n"
<< "Connection: close\r\n"
<< "Content-Length: " << body.size() << "\r\n\r\n"
<< body;
boost::asio::write(socket, boost::asio::buffer(request.str()));
boost::asio::streambuf response;
boost::asio::read_until(socket, response, "\r\n\r\n");
std::istream responseStream(&response);
std::string httpVersion;
unsigned int statusCode = 0;
std::string statusMessage;
responseStream >> httpVersion >> statusCode;
std::getline(responseStream, statusMessage);
if (!responseStream || httpVersion.rfind("HTTP/", 0) != 0)
throw std::runtime_error("invalid HTTP response");
std::string header;
while (std::getline(responseStream, header) && header != "\r")
{
}
std::ostringstream responseBody;
if (response.size())
responseBody << &response;
boost::system::error_code error;
while (boost::asio::read(socket, response, boost::asio::transfer_at_least(1), error))
responseBody << &response;
if (error != boost::asio::error::eof)
throw boost::system::system_error(error);
if (response.size())
responseBody << &response;
if (statusCode < 200 || statusCode >= 300)
{
throw std::runtime_error(
"bridge returned HTTP " + std::to_string(statusCode) + ": " + responseBody.str());
}
return responseBody.str();
}
class PlayerbotsAiBridge
{
public:
static PlayerbotsAiBridge& Instance()
{
static PlayerbotsAiBridge instance;
return instance;
}
void Start()
{
bool expected = false;
if (!_running.compare_exchange_strong(expected, true))
return;
_stopping = false;
_worker = std::thread([this] { Run(); });
LOG_INFO("server.loading", "[PlayerbotsAI] Async bridge worker started");
}
void Stop()
{
if (!_running.exchange(false))
return;
{
std::lock_guard<std::mutex> lock(_requestMutex);
_stopping = true;
}
_requestReady.notify_all();
if (_worker.joinable())
_worker.join();
std::lock_guard<std::mutex> requestLock(_requestMutex);
_requests.clear();
LOG_INFO("server.loading", "[PlayerbotsAI] Async bridge worker stopped");
}
bool Submit(AiRequest request, uint32 maxQueuedRequests)
{
if (!_running)
return false;
std::lock_guard<std::mutex> lock(_requestMutex);
if (_stopping || _requests.size() >= maxQueuedRequests)
return false;
_requests.push_back(std::move(request));
_requestReady.notify_one();
return true;
}
std::deque<AiResponse> TakeResponses()
{
std::lock_guard<std::mutex> lock(_responseMutex);
std::deque<AiResponse> responses;
responses.swap(_responses);
return responses;
}
private:
PlayerbotsAiBridge() = default;
~PlayerbotsAiBridge()
{
Stop();
}
void Run()
{
while (true)
{
AiRequest request;
{
std::unique_lock<std::mutex> lock(_requestMutex);
_requestReady.wait(lock, [this] { return _stopping || !_requests.empty(); });
if (_stopping && _requests.empty())
break;
request = std::move(_requests.front());
_requests.pop_front();
}
try
{
BridgeResponse response = ParseBridgeResponse(ReadHttpBody(
request.host, request.port, request.path, request.body));
std::lock_guard<std::mutex> lock(_responseMutex);
_responses.push_back({
request.botGuid,
request.playerGuid,
request.teamId,
std::move(request.channelName),
std::move(response.command),
std::move(response.text)
});
}
catch (std::exception const& error)
{
LOG_ERROR("server.loading", "[PlayerbotsAI] Bridge request failed: {}", error.what());
}
}
}
std::atomic<bool> _running{false};
bool _stopping{false};
std::thread _worker;
std::mutex _requestMutex;
std::condition_variable _requestReady;
std::deque<AiRequest> _requests;
std::mutex _responseMutex;
std::deque<AiResponse> _responses;
};
bool IsEnabled()
{
return sConfigMgr->GetOption<bool>("PlayerbotsAI.Enabled", false);
}
bool IsAvailableForAll()
{
return sConfigMgr->GetOption<bool>("PlayerbotsAI.AvailableForAll", true);
}
AiRequest BuildRequest(
Player* bot,
Player* player,
std::string channelName,
std::string const& message)
{
std::ostringstream body;
body << '{'
<< "\"bot_guid\":\"" << bot->GetGUID().GetCounter() << "\","
<< "\"player_guid\":\"" << player->GetGUID().GetCounter() << "\","
<< "\"bot_name\":\"" << JsonEscape(bot->GetName()) << "\","
<< "\"player_name\":\"" << JsonEscape(player->GetName()) << "\","
<< "\"message\":\"" << JsonEscape(message) << "\","
<< "\"bot_gender\":" << static_cast<uint32>(bot->getGender()) << ','
<< "\"player_gender\":" << static_cast<uint32>(player->getGender()) << ','
<< "\"bot_class\":" << static_cast<uint32>(bot->getClass()) << ','
<< "\"bot_race\":" << static_cast<uint32>(bot->getRace()) << ','
<< "\"bot_level\":" << static_cast<uint32>(bot->GetLevel()) << ','
<< "\"zone_id\":" << bot->GetZoneId()
<< '}';
return {
bot->GetGUID(),
player->GetGUID(),
bot->GetTeamId(),
std::move(channelName),
sConfigMgr->GetOption<std::string>("PlayerbotsAI.BridgeHost", "playerbots-ai"),
std::to_string(sConfigMgr->GetOption<uint32>("PlayerbotsAI.BridgePort", 8080)),
sConfigMgr->GetOption<std::string>("PlayerbotsAI.BridgePath", "/v1/chat"),
body.str()
};
}
class PlayerbotsAiPlayerScript : public PlayerScript
{
public:
PlayerbotsAiPlayerScript()
: PlayerScript(
"PlayerbotsAiPlayerScript",
{
PLAYERHOOK_ON_BEFORE_SEND_CHAT_MESSAGE,
PLAYERHOOK_CAN_PLAYER_USE_CHANNEL_CHAT
})
{
}
void OnPlayerBeforeSendChatMessage(
Player* player,
uint32& type,
uint32& /*language*/,
std::string& message) override
{
if (type == CHAT_MSG_SAY)
HandleAddressedMessage(player, message, nullptr);
}
bool OnPlayerCanUseChat(
Player* player,
uint32 type,
uint32 /*language*/,
std::string& message,
Channel* channel) override
{
if (type == CHAT_MSG_CHANNEL)
HandleAddressedMessage(player, message, channel);
return true;
}
private:
void HandleAddressedMessage(Player* player, std::string const& message, Channel* channel)
{
if (!IsEnabled() || !player || message.empty())
return;
if (PlayerbotsMgr::instance().GetPlayerbotAI(player))
return;
std::vector<uint32> foldedMessage = DecodeAndCaseFold(message);
Player* selectedBot = nullptr;
std::size_t selectedPosition = foldedMessage.size();
PlayerBotMap bots = sRandomPlayerbotMgr.GetAllBots();
if (PlayerbotMgr* playerbotMgr = PlayerbotsMgr::instance().GetPlayerbotMgr(player))
{
for (auto iterator = playerbotMgr->GetPlayerBotsBegin();
iterator != playerbotMgr->GetPlayerBotsEnd();
++iterator)
{
bots.emplace(iterator->first, iterator->second);
}
}
for (auto const& [guid, bot] : bots)
{
if (!bot || !bot->IsInWorld())
continue;
if (channel)
{
if (!channel->HasMember(guid))
continue;
}
else if (!bot->IsWithinDistInMap(
player, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY)))
{
continue;
}
std::optional<std::size_t> position =
FindCompleteName(foldedMessage, DecodeAndCaseFold(bot->GetName()));
if (position && *position < selectedPosition)
{
selectedBot = bot;
selectedPosition = *position;
}
}
if (!selectedBot)
return;
if (!IsAvailableForAll())
{
std::string const deniedReply = "Разговоры со мной через ИИ сейчас недоступны.";
if (channel)
channel->Say(selectedBot->GetGUID(), deniedReply, LANG_UNIVERSAL);
else
selectedBot->Say(deniedReply, LANG_UNIVERSAL);
return;
}
auto now = std::chrono::steady_clock::now();
uint32 cooldownSeconds =
sConfigMgr->GetOption<uint32>("PlayerbotsAI.PlayerCooldownSeconds", 8);
auto cooldown = std::chrono::seconds(cooldownSeconds);
auto found = _lastRequest.find(player->GetGUID());
if (found != _lastRequest.end() && now - found->second < cooldown)
return;
uint32 maxQueuedRequests =
std::max<uint32>(1, sConfigMgr->GetOption<uint32>("PlayerbotsAI.MaxQueuedRequests", 32));
if (!PlayerbotsAiBridge::Instance().Submit(
BuildRequest(
selectedBot,
player,
channel ? channel->GetName() : std::string(),
message),
maxQueuedRequests))
{
LOG_WARN("server.loading", "[PlayerbotsAI] Request queue is unavailable or full");
return;
}
_lastRequest[player->GetGUID()] = now;
if (sConfigMgr->GetOption<bool>("PlayerbotsAI.Debug", false))
{
LOG_INFO(
"server.loading",
"[PlayerbotsAI] {} addressed bot {} in {}",
player->GetName(),
selectedBot->GetName(),
channel ? channel->GetName() : "/say");
}
}
std::map<ObjectGuid, std::chrono::steady_clock::time_point> _lastRequest;
};
class PlayerbotsAiWorldScript : public WorldScript
{
public:
PlayerbotsAiWorldScript()
: WorldScript(
"PlayerbotsAiWorldScript",
{WORLDHOOK_ON_STARTUP, WORLDHOOK_ON_UPDATE, WORLDHOOK_ON_SHUTDOWN})
{
}
void OnStartup() override
{
if (IsEnabled())
PlayerbotsAiBridge::Instance().Start();
}
void OnUpdate(uint32 /*diff*/) override
{
if (!IsEnabled())
return;
uint32 maxCharacters =
sConfigMgr->GetOption<uint32>("PlayerbotsAI.MaxReplyCharacters", 240);
for (AiResponse& response : PlayerbotsAiBridge::Instance().TakeResponses())
{
Player* bot = ObjectAccessor::FindPlayer(response.botGuid);
if (!bot || !bot->IsInWorld() || !PlayerbotsMgr::instance().GetPlayerbotAI(bot))
continue;
Player* player = ObjectAccessor::FindPlayer(response.playerGuid);
if (!player || !player->IsInWorld())
continue;
Channel* channel = nullptr;
if (response.channelName.empty())
{
if (!bot->IsWithinDistInMap(
player, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY)))
{
continue;
}
}
else
{
ChannelMgr* channelManager = ChannelMgr::forTeam(response.teamId);
channel = channelManager
? channelManager->GetChannel(response.channelName, bot, false)
: nullptr;
if (!channel || !channel->HasMember(bot->GetGUID()))
continue;
}
if (sConfigMgr->GetOption<bool>("PlayerbotsAI.CommandsEnabled", true)
&& IsAllowedPlayerbotCommand(response.command))
{
PlayerbotAI* botAi = PlayerbotsMgr::instance().GetPlayerbotAI(bot);
botAi->HandleCommand(CHAT_MSG_WHISPER, response.command, player);
}
std::string reply = NormalizeReply(std::move(response.text), maxCharacters);
if (!reply.empty())
{
if (channel)
channel->Say(bot->GetGUID(), reply, LANG_UNIVERSAL);
else
bot->Say(reply, LANG_UNIVERSAL);
}
}
}
void OnShutdown() override
{
PlayerbotsAiBridge::Instance().Stop();
}
};
}
void AddSC_mod_playerbots_ai()
{
new PlayerbotsAiPlayerScript();
new PlayerbotsAiWorldScript();
}
@@ -1,6 +0,0 @@
void AddSC_mod_playerbots_ai();
void Addmod_playerbots_aiScripts()
{
AddSC_mod_playerbots_ai();
}
@@ -84,10 +84,6 @@
# Enable or disable Playerbots module
AiPlayerbot.Enabled = 1
# Use the MoonWell Cyrillic name and guild-name pools.
# Existing RNDbot characters can be migrated with localize-playerbots-ru.sh.
AiPlayerbot.UseRussianNames = 0
# Enable randombot system
AiPlayerbot.RandomBotAutologin = 1
@@ -55,7 +55,7 @@ bool AcceptInvitationAction::Execute(Event event)
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
botAI->Reset();
botAI->TellMaster("Здравствуйте.");
botAI->TellMaster("Hello");
if (sPlayerbotAIConfig.summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig.sightDistance)
{
@@ -18,7 +18,7 @@ bool AcceptAllQuestsAction::ProcessQuest(Quest const* quest, Object* questGiver)
if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
LOG_INFO("playerbots", "{} => Quest [{}] accepted", bot->GetName(), quest->GetTitle());
bot->Say("Задание [" + text_quest + "] accepted", LANG_UNIVERSAL);
bot->Say("Quest [" + text_quest + "] accepted", LANG_UNIVERSAL);
}
return true;
@@ -113,7 +113,7 @@ bool AcceptQuestShareAction::Execute(Event event)
if (bot->HasQuest(quest))
{
bot->SetDivider(ObjectGuid::Empty);
botAI->TellError("У меня уже есть это задание");
botAI->TellError("I have this quest");
return false;
}
@@ -121,7 +121,7 @@ bool AcceptQuestShareAction::Execute(Event event)
{
// can't take quest
bot->SetDivider(ObjectGuid::Empty);
botAI->TellError("Я не могу взять это задание");
botAI->TellError("I can't take this quest");
return false;
}
@@ -149,7 +149,7 @@ bool AcceptQuestShareAction::Execute(Event event)
bot->CastSpell(bot, qInfo->GetSrcSpell(), true);
}
botAI->TellMaster("Задание принято.");
botAI->TellMaster("Quest accepted");
return true;
}
@@ -36,7 +36,7 @@ bool ReachAreaTriggerAction::Execute(Event event)
if (bot->GetMapId() != at->map)
{
botAI->TellError("Я не последую за вами: вы слишком далеко");
botAI->TellError("I won't follow: too far away");
return true;
}
@@ -51,7 +51,7 @@ bool ReachAreaTriggerAction::Execute(Event event)
float distance = bot->GetDistance(at->x, at->y, at->z);
float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay;
botAI->TellError("Подождите меня");
botAI->TellError("Wait for me");
botAI->SetNextCheckDelay(delay);
context->GetValue<LastMovement&>("last area trigger")->Get().lastAreaTrigger = triggerId;
@@ -76,6 +76,6 @@ bool AreaTriggerAction::Execute(Event /*event*/)
p.rpos(0);
bot->GetSession()->HandleAreaTriggerOpcode(p);
botAI->TellMaster("Здравствуйте.");
botAI->TellMaster("Hello");
return true;
}
@@ -31,7 +31,7 @@ bool ArenaTeamAcceptAction::Execute(Event event)
if (bot->GetArenaTeamId(at->GetSlot()))
{
// bot is already in an arena team
bot->Say("У меня уже есть такая команда", LANG_UNIVERSAL);
bot->Say("Sorry, I am already in such team", LANG_UNIVERSAL);
accept = false;
}
@@ -39,7 +39,7 @@ bool ArenaTeamAcceptAction::Execute(Event event)
{
WorldPacket data(CMSG_ARENA_TEAM_ACCEPT);
bot->GetSession()->HandleArenaTeamAcceptOpcode(data);
bot->Say("Спасибо за приглашение!", LANG_UNIVERSAL);
bot->Say("Thanks for the invite!", LANG_UNIVERSAL);
LOG_INFO("playerbots", "Bot {} <{}> accepts Arena Team invite", bot->GetGUID().ToString().c_str(),
bot->GetName().c_str());
return true;
@@ -37,7 +37,7 @@ bool AttackMyTargetAction::Execute(Event /*event*/)
if (!guid)
{
if (verbose)
botAI->TellError("У вас нет цели");
botAI->TellError("You have no target");
return false;
}
@@ -63,7 +63,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
{
if (verbose)
botAI->TellError("Я не могу атаковать в полёте");
botAI->TellError("I cannot attack in flight");
return false;
}
@@ -71,7 +71,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (!target)
{
if (verbose)
botAI->TellError("У меня нет цели");
botAI->TellError("I have no target");
return false;
}
@@ -91,7 +91,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
sPlayerbotAIConfig.IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
{
if (verbose)
botAI->TellError("Я не могу атаковать игроков в зонах, где запрещено PvP.");
botAI->TellError("I cannot attack other players in PvP prohibited areas.");
return false;
}
@@ -123,7 +123,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (sameTarget && inCombat && sameAttackMode)
{
if (verbose)
botAI->TellError("Я уже атакую: " + std::string(target->GetName()) + ".");
botAI->TellError("I am already attacking " + std::string(target->GetName()) + ".");
return false;
}
@@ -131,7 +131,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (!bot->IsValidAttackTarget(target))
{
if (verbose)
botAI->TellError("Я не могу атаковать недопустимую цель.");
botAI->TellError("I cannot attack an invalid target.");
return false;
}
@@ -54,7 +54,7 @@ void AutoMaintenanceOnLevelupAction::AutoLearnSpell()
{
std::string const temp = out.str();
out.seekp(0);
out << "Изученные заклинания: ";
out << "Learned spells: ";
out << temp;
out.seekp(-2, out.cur);
out << ".";
@@ -23,7 +23,7 @@ bool BankAction::Execute(Event event)
return ExecuteBank(text, npc);
}
botAI->TellError("Поблизости нет банкира");
botAI->TellError("Cannot find banker nearby");
return false;
}
@@ -82,7 +82,7 @@ bool BankAction::Withdraw(uint32 itemid)
bot->StoreItem(dest, pItem, true);
std::ostringstream out;
out << "получено: " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " из банка";
out << "got " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " from bank";
botAI->TellMaster(out.str());
return true;
}
@@ -102,14 +102,14 @@ bool BankAction::Deposit(Item* pItem)
bot->RemoveItem(pItem->GetBagSlot(), pItem->GetSlot(), true);
bot->BankItem(dest, pItem, true);
out << "помещено: " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " в банк";
out << "put " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " to bank";
botAI->TellMaster(out.str());
return true;
}
void BankAction::ListItems()
{
botAI->TellMaster("=== Банк ===");
botAI->TellMaster("=== Bank ===");
std::map<uint32, uint32> items;
std::map<uint32, bool> soulbound;
@@ -1278,13 +1278,13 @@ bool BGTactics::HandleConsoleCommand(ChatHandler* handler, char const* args)
{
if (!sPlayerbotAIConfig.enabled)
{
handler->PSendSysMessage("|cffff0000Система Playerbot сейчас отключена!");
handler->PSendSysMessage("|cffff0000Playerbot system is currently disabled!");
return true;
}
WorldSession* session = handler->GetSession();
if (!session)
{
handler->PSendSysMessage("Команду можно использовать только из активной игровой сессии");
handler->PSendSysMessage("Command can only be used from an active session");
return true;
}
std::string const commandOutput = HandleConsoleCommandPrivate(session, args);
@@ -4304,7 +4304,7 @@ bool ArenaTactics::Execute(Event /*event*/)
float x, y, z;
target->GetPosition(x, y, z);
botAI->TellMasterNoFacing("Меняю позицию, чтобы выйти из поля зрения цели!");
botAI->TellMasterNoFacing("Repositioning to exit the LoS target!");
return MoveTo(target->GetMapId(), x + frand(-1, +1), y + frand(-1, +1), z, false, true);
}
}
@@ -64,19 +64,19 @@ void BuffAction::TellHeader(uint32 subClass)
switch (subClass)
{
case ITEM_SUBCLASS_ELIXIR:
botAI->TellMaster("--- Эликсиры ---");
botAI->TellMaster("--- Elixir ---");
return;
case ITEM_SUBCLASS_FLASK:
botAI->TellMaster("--- Настои ---");
botAI->TellMaster("--- Flask ---");
return;
case ITEM_SUBCLASS_SCROLL:
botAI->TellMaster("--- Свитки ---");
botAI->TellMaster("--- Scroll ---");
return;
case ITEM_SUBCLASS_FOOD:
botAI->TellMaster("--- Еда ---");
botAI->TellMaster("--- Food ---");
return;
case ITEM_SUBCLASS_ITEM_ENHANCEMENT:
botAI->TellMaster("--- Чары ---");
botAI->TellMaster("--- Enchant ---");
return;
}
}
@@ -197,7 +197,7 @@ bool BuyAction::Execute(Event event)
if (!result)
{
std::ostringstream out;
out << "Никто не продаёт: " << ChatHelper::FormatItem(proto) << " поблизости";
out << "Nobody sells " << ChatHelper::FormatItem(proto) << " nearby";
botAI->TellMaster(out.str());
continue;
}
@@ -215,7 +215,7 @@ bool BuyAction::Execute(Event event)
if (!vendored)
{
botAI->TellError("Поблизости нет торговцев");
botAI->TellError("There are no vendors nearby");
return false;
}
@@ -248,7 +248,7 @@ bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, Ite
if (newCount > oldCount)
{
std::ostringstream out;
out << "Покупаю: " << ChatHelper::FormatItem(proto);
out << "Buying " << ChatHelper::FormatItem(proto);
botAI->TellMaster(out.str());
return true;
}
@@ -117,7 +117,7 @@ bool CastCustomSpellAction::Execute(Event event)
std::ostringstream msg;
if (!spell)
{
msg << "Неизвестное заклинание: " << text;
msg << "Unknown spell " << text;
botAI->TellError(msg.str());
return false;
}
@@ -125,7 +125,7 @@ bool CastCustomSpellAction::Execute(Event event)
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell);
if (!spellInfo)
{
msg << "Неизвестное заклинание: " << text;
msg << "Unknown spell " << text;
botAI->TellError(msg.str());
return false;
}
@@ -154,7 +154,7 @@ bool CastCustomSpellAction::Execute(Event event)
if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget))
{
msg << "Не удалось применить: " << spellName.str();
msg << "Cannot cast " << spellName.str();
botAI->TellError(msg.str());
return false;
}
@@ -162,7 +162,7 @@ bool CastCustomSpellAction::Execute(Event event)
bool result = spell ? botAI->CastSpell(spell, target, itemTarget) : botAI->CastSpell(text, target, itemTarget);
if (result)
{
msg << "Применяю: " << spellName.str();
msg << "Casting " << spellName.str();
if (castCount > 1)
{
@@ -176,7 +176,7 @@ bool CastCustomSpellAction::Execute(Event event)
}
else
{
msg << "Применение " << spellName.str() << " завершилось ошибкой";
msg << "Cast " << spellName.str() << " is failed";
botAI->TellError(msg.str());
}
@@ -15,7 +15,7 @@ bool ChangeChatAction::Execute(Event event)
if (parsed == CHAT_MSG_SYSTEM)
{
std::ostringstream out;
out << "Текущий канал чата: " << chat->FormatChat(*context->GetValue<ChatMsg>("chat"));
out << "Current chat is " << chat->FormatChat(*context->GetValue<ChatMsg>("chat"));
botAI->TellMaster(out);
}
else
@@ -23,7 +23,7 @@ bool ChangeChatAction::Execute(Event event)
context->GetValue<ChatMsg>("chat")->Set(parsed);
std::ostringstream out;
out << "Канал чата установлен: " << chat->FormatChat(parsed);
out << "Chat set to " << chat->FormatChat(parsed);
botAI->TellMaster(out);
}
@@ -45,7 +45,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event)
{
if (text.find("loot") != std::string::npos || text.find("gather") != std::string::npos)
{
botAI->TellError("Можно изменять любую стратегию, кроме loot и gather");
botAI->TellError("You can change any strategy except loot and gather");
return false;
}
}
@@ -38,7 +38,7 @@ bool ChangeTalentsAction::Execute(Event event)
if (param.find("switch 1") != std::string::npos)
{
bot->ActivateSpec(0);
out << "Активирован первый набор талантов";
out << "Active first talent";
botAI->ResetStrategies();
}
else if (param.find("switch 2") != std::string::npos)
@@ -49,7 +49,7 @@ bool ChangeTalentsAction::Execute(Event event)
bot->CastSpell(bot, 63624, true, nullptr, nullptr, bot->GetGUID());
}
bot->ActivateSpec(1);
out << "Активирован второй набор талантов";
out << "Active second talent";
botAI->ResetStrategies();
}
}
@@ -57,7 +57,7 @@ bool ChangeTalentsAction::Execute(Event event)
{
PlayerbotFactory factory(bot, bot->GetLevel());
factory.InitTalentsTree(true);
out << "Автоматический выбор талантов включён";
out << "Auto pick talents";
botAI->ResetStrategies();
}
else if (param.find("spec list") != std::string::npos)
@@ -78,13 +78,13 @@ bool ChangeTalentsAction::Execute(Event event)
}
else
{
out << "Неизвестная команда.";
out << "Unknown command.";
}
}
else
{
uint32 tab = AiFactory::GetPlayerSpecTab(bot);
out << "Моя текущая специализация: "
out << "My current talent spec is: "
<< "|h|cffffffff";
out << chat->FormatClass(bot, tab) << "\n";
out << TalentsHelp();
@@ -98,7 +98,7 @@ bool ChangeTalentsAction::Execute(Event event)
std::string ChangeTalentsAction::TalentsHelp()
{
std::ostringstream out;
out << "Команды талантов: talents switch <1/2>, talents autopick, talents spec list, "
out << "Talents usage: talents switch <1/2>, talents autopick, talents spec list, "
"talents spec <specName>, talents apply <link>.";
return out.str();
}
@@ -127,7 +127,7 @@ std::string ChangeTalentsAction::SpecList()
out << tabCount[0] << "-" << tabCount[1] << "-" << tabCount[2] << ")";
botAI->TellMasterNoFacing(out.str());
}
out << "Найдено: " << specFound << " специализаций";
out << "Total " << specFound << " specs found";
return out.str();
}
@@ -149,12 +149,12 @@ std::string ChangeTalentsAction::SpecPick(std::string param)
factory.InitGlyphs(false);
std::ostringstream out;
out << "Выбираю: " << sPlayerbotAIConfig.premadeSpecName[cls][specNo];
out << "Picking " << sPlayerbotAIConfig.premadeSpecName[cls][specNo];
return out.str();
}
}
std::ostringstream out;
out << "Специализация " << param << " не найдена";
out << "Spec " << param << " not found";
return out.str();
}
@@ -165,11 +165,11 @@ std::string ChangeTalentsAction::SpecApply(std::string param)
std::vector<std::vector<uint32>> parsedSpecLink = PlayerbotAIConfig::ParseTempTalentsOrder(cls, param);
if (parsedSpecLink.size() == 0)
{
out << "Неверная ссылка: " << param;
out << "Invalid link " << param;
return out.str();
}
PlayerbotFactory::InitTalentsByParsedSpecLink(bot, parsedSpecLink, true);
out << "Применяю: " << param;
out << "Applying " << param;
return out.str();
}
@@ -89,7 +89,7 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
if (moved)
{
botAI->TellMaster("Следую за целью.");
botAI->TellMaster("Following");
return true;
}
}
@@ -101,10 +101,10 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
if (bot->isDead())
{
bot->ResurrectPlayer(1.0f, false);
botAI->TellMasterNoFacing("Возвращение из могилы!");
botAI->TellMasterNoFacing("Back from the grave!");
}
else
botAI->TellMaster("Вы слишком далеко! Я скоро подойду.");
botAI->TellMaster("You are too far away from me! I will there soon.");
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(master->GetMapId(), master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(),
@@ -112,7 +112,7 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
}
*/
botAI->TellMaster("Следую за целью.");
botAI->TellMaster("Following");
return true;
}
@@ -129,7 +129,7 @@ bool StayChatShortcutAction::Execute(Event /*event*/)
SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
botAI->TellMaster("Остаюсь на месте.");
botAI->TellMaster("Staying");
return true;
}
@@ -144,7 +144,7 @@ bool MoveFromGroupChatShortcutAction::Execute(Event /*event*/)
botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT);
botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT);
botAI->TellMaster("Отхожу от группы");
botAI->TellMaster("Moving away from group");
return true;
}
@@ -163,11 +163,11 @@ bool FleeChatShortcutAction::Execute(Event /*event*/)
if (bot->GetMapId() != master->GetMapId() || bot->GetDistance(master) > sPlayerbotAIConfig.sightDistance)
{
botAI->TellError("Я не могу отступать вместе с вами — вы слишком далеко");
botAI->TellError("I will not flee with you - too far away");
return true;
}
botAI->TellMaster("Отступаю.");
botAI->TellMaster("Fleeing");
return true;
}
@@ -184,7 +184,7 @@ bool GoawayChatShortcutAction::Execute(Event /*event*/)
ResetReturnPosition();
ResetStayPosition();
botAI->TellMaster("Отступаю");
botAI->TellMaster("Running away");
return true;
}
@@ -200,7 +200,7 @@ bool GrindChatShortcutAction::Execute(Event /*event*/)
ResetReturnPosition();
ResetStayPosition();
botAI->TellMaster("Фармлю.");
botAI->TellMaster("Grinding");
return true;
}
@@ -220,7 +220,7 @@ bool TankAttackChatShortcutAction::Execute(Event /*event*/)
ResetReturnPosition();
ResetStayPosition();
botAI->TellMaster("Атакую.");
botAI->TellMaster("Attacking");
return true;
}
@@ -236,7 +236,7 @@ bool MaxDpsChatShortcutAction::Execute(Event /*event*/)
botAI->Reset();
botAI->ChangeStrategy("-threat,-conserve mana,-cast time,+dps debuff,+boost", BOT_STATE_COMBAT);
botAI->TellMaster("Максимальный урон!");
botAI->TellMaster("Max DPS!");
return true;
}
@@ -250,6 +250,6 @@ bool BwlChatShortcutAction::Execute(Event /*event*/)
botAI->Reset();
botAI->ChangeStrategy("+bwl", BOT_STATE_NON_COMBAT);
botAI->ChangeStrategy("+bwl", BOT_STATE_COMBAT);
botAI->TellMasterNoFacing("Стратегии Логова Крыла Тьмы включены!");
botAI->TellMasterNoFacing("Add Bwl Strategies!");
return true;
}
@@ -258,7 +258,7 @@ bool ChooseRpgTargetAction::Execute(Event /*event*/)
if (botAI->HasStrategy("debug", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject())
{
std::ostringstream out;
out << "найдено: ";
out << "found: ";
out << chat->FormatWorldobject(guidP.GetWorldObject());
out << " " << relevances.front();
@@ -169,7 +169,7 @@ bool AttackRtiTargetAction::Execute(Event /*event*/)
}
}
else
botAI->TellError("Я не вижу назначенную групповую цель для атаки");
botAI->TellError("I dont see my rti attack target");
return false;
}
@@ -244,15 +244,15 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
std::string Sub;
if (newTarget->isGroupCopy())
out << "Следую за группой: ";
out << "Following group ";
else if (oldDestination && oldDestination == destination)
out << "Продолжаю: ";
out << "Continuing ";
else
out << "Путешествую: ";
out << "Traveling ";
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
out << " за " << chat->FormatQuest(quest);
out << " for " << chat->FormatQuest(quest);
out << " to " << QuestDestination->getTitle();
@@ -265,20 +265,20 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
WorldPosition botLocation(bot);
if (newTarget->isGroupCopy())
out << "Следую за группой: ";
out << "Following group ";
else if (oldDestination && oldDestination == destination)
out << "Продолжаю: ";
out << "Continuing ";
else
out << "Путешествую: ";
out << "Traveling ";
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
out << " за ";
out << " for ";
if (AI_VALUE2(bool, "group or", "should sell,can sell"))
out << "продаю предметы";
out << "selling items";
else if (AI_VALUE2(bool, "group or", "should repair,can repair"))
out << "ремонтируюсь";
out << "repairing";
else
out << "rpg";
@@ -293,11 +293,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
WorldPosition botLocation(bot);
if (newTarget->isGroupCopy())
out << "Следую за группой: ";
out << "Following group ";
else if (oldDestination && oldDestination == destination)
out << "Продолжаю: ";
out << "Continuing ";
else
out << "Путешествую: ";
out << "Traveling ";
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
@@ -314,11 +314,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
WorldPosition botLocation(bot);
if (newTarget->isGroupCopy())
out << "Следую за группой: ";
out << "Following group ";
else if (oldDestination && oldDestination == destination)
out << "Продолжаю: ";
out << "Continuing ";
else
out << "Путешествую: ";
out << "Traveling ";
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
@@ -335,11 +335,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
WorldPosition botLocation(bot);
if (newTarget->isGroupCopy())
out << "Следую за группой: ";
out << "Following group ";
else if (oldDestination && oldDestination == destination)
out << "Продолжаю: ";
out << "Continuing ";
else
out << "Путешествую: ";
out << "Traveling ";
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
@@ -353,7 +353,7 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
{
if (!oldTarget->getDestination() || oldTarget->getDestination()->getName() != "NullTravelDestination")
{
botAI->TellMaster("Некуда идти. Немного подожду.");
botAI->TellMaster("No where to travel. Idling a bit.");
}
}
}
@@ -31,7 +31,7 @@ bool CustomStrategyEditAction::Execute(Event event)
bool CustomStrategyEditAction::PrintHelp()
{
botAI->TellMaster("=== Пользовательские стратегии ===");
botAI->TellMaster("=== Custom strategies ===");
uint32 owner = botAI->GetBot()->GetGUID().GetCounter();
@@ -48,7 +48,7 @@ bool CustomStrategyEditAction::PrintHelp()
} while (result->NextRow());
}
botAI->TellMaster("Использование: cs <имя> <индекс> <команда>");
botAI->TellMaster("Usage: cs <name> <idx> <command>");
return false;
}
@@ -79,7 +79,7 @@ bool DebugAction::Execute(Event event)
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot);
std::ostringstream out;
out << "Направляюсь к: " << dest->getTitle() << ": ";
out << "Traveling to " << dest->getTitle() << ": ";
for (auto node : route.getNodes())
{
@@ -92,7 +92,7 @@ bool DebugAction::Execute(Event event)
}
else
{
botAI->TellMasterNoFacing("Точка назначения " + destination + " not found.");
botAI->TellMasterNoFacing("Destination " + destination + " not found.");
return true;
}
}
@@ -104,7 +104,7 @@ bool DebugAction::Execute(Event event)
if (!quest)
{
botAI->TellMasterNoFacing("Задание " + text.substr(6) + " not found.");
botAI->TellMasterNoFacing("Quest " + text.substr(6) + " not found.");
return false;
}
@@ -164,7 +164,7 @@ bool DebugAction::Execute(Event event)
else if (text.find("bquest") != std::string::npos)
{
std::ostringstream out;
out << "ошибочные задания:";
out << "bad quests:";
// uint32 noT = 0, noG = 0, noO = 0; //not used, line marked for removal.
@@ -205,7 +205,7 @@ bool DebugAction::Execute(Event event)
endNode->setLinked(false);
}
botAI->TellMasterNoFacing("Узел " + name + " created.");
botAI->TellMasterNoFacing("Node " + name + " created.");
TravelNodeMap::instance().setHasToGen();
@@ -222,12 +222,12 @@ bool DebugAction::Execute(Event event)
if (startNode->isImportant())
{
botAI->TellMasterNoFacing("Не удалось удалить узел.");
botAI->TellMasterNoFacing("Node can not be removed.");
}
TravelNodeMap::instance().m_nMapMtx.lock();
TravelNodeMap::instance().removeNode(startNode);
botAI->TellMasterNoFacing("Узел удалён.");
botAI->TellMasterNoFacing("Node removed.");
TravelNodeMap::instance().m_nMapMtx.unlock();
TravelNodeMap::instance().setHasToGen();
@@ -51,10 +51,10 @@ bool DropQuestAction::Execute(Event event)
const Quest* pQuest = sObjectMgr->GetQuestTemplate(entry);
const std::string text_quest = ChatHelper::FormatQuest(pQuest);
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), pQuest->GetTitle());
bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL);
bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL);
}
botAI->TellMaster("Задание удалено.");
botAI->TellMaster("Quest removed");
return true;
}
@@ -63,7 +63,7 @@ bool CleanQuestLogAction::Execute(Event event)
Player* requester = event.getOwner() ? event.getOwner() : GetMaster();
if (!requester)
{
botAI->TellMaster("Не удалось определить владельца события");
botAI->TellMaster("No event owner detected");
return false;
}
@@ -75,7 +75,7 @@ bool CleanQuestLogAction::Execute(Event event)
// Only output this message if "debug rpg" strategy is enabled
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
botAI->TellMaster("Получена команда очистки журнала: удаляю серые и незначительные задания...");
botAI->TellMaster("Clean Quest Log command received, removing grey/trivial quests...");
}
uint8 botLevel = bot->GetLevel(); // Get bot's level
@@ -127,7 +127,7 @@ bool CleanQuestLogAction::Execute(Event event)
// Output only if "debug rpg" strategy is enabled
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey).");
botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey).");
}
// Remove quest
@@ -143,12 +143,12 @@ bool CleanQuestLogAction::Execute(Event event)
{
const std::string text_quest = ChatHelper::FormatQuest(quest);
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle());
bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL);
bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL);
}
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] has been removed.");
botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] has been removed.");
}
}
else
@@ -156,7 +156,7 @@ bool CleanQuestLogAction::Execute(Event event)
// Only output if "debug rpg" strategy is enabled
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] is not trivial and will be kept.");
botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] is not trivial and will be kept.");
}
}
}
@@ -234,9 +234,9 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG
{
const std::string text_quest = ChatHelper::FormatQuest(quest);
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle());
bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL);
bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL);
}
botAI->TellMaster("Задание удалено." + chat->FormatQuest(quest));
botAI->TellMaster("Quest removed" + chat->FormatQuest(quest));
}
}
@@ -163,7 +163,7 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal)
if (botAI->GetMaster() == source)
{
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
botAI->TellMasterNoFacing("Хорошо... Останусь здесь...");
botAI->TellMasterNoFacing("Fine.. I'll stay right here..");
}
break;
case TEXT_EMOTE_BECKON:
@@ -171,7 +171,7 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal)
if (botAI->GetMaster() == source)
{
botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT);
botAI->TellMasterNoFacing("Куда бы вы ни пошли, я последую за вами...");
botAI->TellMasterNoFacing("Wherever you go, I'll follow..");
}
break;
case TEXT_EMOTE_WAVE:
@@ -579,36 +579,36 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal)
break;
/*case TEXT_EMOTE_BADFEELING:
bot->HandleEmoteCommand(EMOTE_ONESHOT_QUESTION);
bot->Say("Теперь осталось дождаться зловещей музыки...", LANG_UNIVERSAL);
bot->Say("I'm just waiting for the ominous music now...", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_MAP:
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
bot->Say("Неееет... Нельзя было просто спросить дорогу?", LANG_UNIVERSAL);
bot->Say("Noooooooo.. you just couldn't ask for directions, huh?", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_IDEA:
case TEXT_EMOTE_THINK:
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
bot->Say("Ох... Ещё одна гениальная идея...", LANG_UNIVERSAL);
bot->Say("Oh boy.. another genius idea...", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_OFFER:
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
bot->Say("Нет, спасибо... В прошлой деревне уже хватило", LANG_UNIVERSAL);
bot->Say("No thanks.. I had some back at the last village", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_PET:
bot->HandleEmoteCommand(EMOTE_ONESHOT_ROAR);
bot->Say("Я похож на собаку?!", LANG_UNIVERSAL);
bot->Say("Do I look like a dog to you?!", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_ROLLEYES:
bot->HandleEmoteCommand(EMOTE_ONESHOT_POINT);
bot->Say("Продолжайте — и глаза скоро закатятся навсегда...", LANG_UNIVERSAL);
bot->Say("Keep doing that and I'll roll those eyes right out of your head..", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_SING:
bot->HandleEmoteCommand(EMOTE_ONESHOT_APPLAUD);
bot->Say("Прекрасно... Просто прекрасно...", LANG_UNIVERSAL);
bot->Say("Lovely... just lovely..", LANG_UNIVERSAL);
break;
case TEXT_EMOTE_COVEREARS:
bot->HandleEmoteCommand(EMOTE_ONESHOT_EXCLAMATION);
bot->Yell("Думаете, это вам поможет?!", LANG_UNIVERSAL);
bot->Yell("You think that's going to help you?!", LANG_UNIVERSAL);
break;*/
default:
// return false;
@@ -75,7 +75,7 @@ void EquipAction::EquipItem(Item* item)
{
bot->SetAmmo(itemId);
std::ostringstream out;
out << "Надеваю: " << chat->FormatItem(itemProto);
out << "equipping " << chat->FormatItem(itemProto);
botAI->TellMaster(out);
return;
}
@@ -112,7 +112,7 @@ void EquipAction::EquipItem(Item* item)
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
std::ostringstream out;
out << "Надеваю: " << chat->FormatItem(itemProto) << " в ячейку оружия дальнего боя";
out << "Equipping " << chat->FormatItem(itemProto) << " in ranged slot";
botAI->TellMaster(out);
return;
}
@@ -227,7 +227,7 @@ void EquipAction::EquipItem(Item* item)
}
std::ostringstream out;
out << "Надеваю: " << chat->FormatItem(itemProto) << " в правую руку";
out << "Equipping " << chat->FormatItem(itemProto) << " in main hand";
botAI->TellMaster(out);
return;
}
@@ -244,7 +244,7 @@ void EquipAction::EquipItem(Item* item)
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
std::ostringstream out;
out << "Надеваю: " << chat->FormatItem(itemProto) << " в левую руку";
out << "Equipping " << chat->FormatItem(itemProto) << " in offhand";
botAI->TellMaster(out);
return;
}
@@ -324,7 +324,7 @@ void EquipAction::EquipItem(Item* item)
}
std::ostringstream out;
out << "Надеваю: " << chat->FormatItem(itemProto);
out << "Equipping " << chat->FormatItem(itemProto);
botAI->TellMaster(out);
}
@@ -106,7 +106,7 @@ bool EquipGlyphsAction::Execute(Event event)
std::vector<GlyphInfo const*> glyphs;
if (!CollectGlyphs(itemIds, glyphs))
{
botAI->TellMaster("Использование: glyph equip <6 ID символов> (3 больших и 3 малых).");
botAI->TellMaster("Usage: glyph equip <6 glyph item IDs> (3 major, 3 minor).");
return false;
}
@@ -144,12 +144,12 @@ bool EquipGlyphsAction::Execute(Event event)
if (!placed)
{
botAI->TellMaster("Недостаточно свободных ячеек для всех символов.");
botAI->TellMaster("Not enought empty sockets for all glyphs.");
return false;
}
}
botAI->TellMaster("Символы обновлены.");
botAI->TellMaster("Glyphs updated.");
// Flag for custom glyphs
botAI->GetAiObjectContext()->GetValue<bool>("custom_glyphs")->Set(true);
@@ -10,7 +10,7 @@
bool FlagAction::TellUsage()
{
botAI->TellError("Использование: flag cloak/helm/pvp on/set/off/clear/toggle/?");
botAI->TellError("Usage: flag cloak/helm/pvp on/set/off/clear/toggle/?");
return false;
}
@@ -130,15 +130,15 @@ bool FleeToGroupLeaderAction::Execute(Event /*event*/)
if (distance < sPlayerbotAIConfig.reactDistance * 3)
{
if (!urand(0, 3))
botAI->TellMaster("Я рядом, подождите меня!");
botAI->TellMaster("I am close, wait for me!");
}
else if (distance < 1000)
{
if (!urand(0, 10))
botAI->TellMaster("Я направляюсь к вам.");
botAI->TellMaster("I heading to your position.");
}
else if (!urand(0, 20))
botAI->TellMaster("Я иду к вам.");
botAI->TellMaster("I am traveling to your position.");
botAI->SetNextCheckDelay(3000);
@@ -112,7 +112,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event /*event*/)
// Debug message if pet spells have been toggled and debug is enabled
if (toggled && sPlayerbotAIConfig.petChatCommandDebug == 1)
botAI->TellMaster("Автоматическое применение способностей питомца переключено.");
botAI->TellMaster("Pet autocast spells have been toggled.");
return toggled;
}
@@ -178,7 +178,7 @@ bool SetPetStanceAction::Execute(Event /*event*/)
// If there are no controlled pets or guardians, notify the player and exit
if (targets.empty())
{
botAI->TellError("У вас нет питомца или стража.");
botAI->TellError("You have no pet or guardian pet.");
return false;
}
@@ -220,7 +220,7 @@ bool SetPetStanceAction::Execute(Event /*event*/)
// If debug is enabled in config, inform the master of the new stance
if (sPlayerbotAIConfig.petChatCommandDebug == 1)
botAI->TellMaster("Режим питомца: " + stanceText + " (applied to all pets/guardians).");
botAI->TellMaster("Pet stance set to " + stanceText + " (applied to all pets/guardians).");
return true;
}
@@ -45,13 +45,13 @@ bool GiveItemAction::Execute(Event /*event*/)
moved = true;
std::ostringstream out;
out << "Получено: " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " от " << bot->GetName();
out << "Got " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " from " << bot->GetName();
receiverAi->TellMasterNoFacing(out.str());
}
else
{
std::ostringstream out;
out << "Не удалось получить: " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " от "
out << "Cannot get " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " from "
<< bot->GetName() << "- my bags are full";
receiverAi->TellError(out.str());
}
@@ -30,7 +30,7 @@ bool GoAction::Execute(Event event)
Map2ZoneCoordinates(x, y, bot->GetZoneId());
std::ostringstream out;
out << "Я нахожусь в точке " << x << "," << y;
out << "I am at " << x << "," << y;
botAI->TellMaster(out.str());
return true;
}
@@ -53,14 +53,14 @@ bool GoAction::Execute(Event event)
target->setForced(true);
std::ostringstream out;
out << "Направляюсь к: " << dest->getTitle();
out << "Traveling to " << dest->getTitle();
botAI->TellMasterNoFacing(out.str());
return true;
}
else
{
botAI->TellMasterNoFacing("Цель путешествия сброшена");
botAI->TellMasterNoFacing("Clearing travel target");
target->setTarget(TravelMgr::instance().nullTravelDestination, TravelMgr::instance().nullWorldPosition);
target->setForced(false);
return true;
@@ -78,12 +78,12 @@ bool GoAction::Execute(Event event)
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, go),
sPlayerbotAIConfig.reactDistance))
{
botAI->TellError("Слишком далеко.");
botAI->TellError("It is too far away");
return false;
}
std::ostringstream out;
out << "Двигаюсь к: " << ChatHelper::FormatGameobject(go);
out << "Moving to " << ChatHelper::FormatGameobject(go);
botAI->TellMasterNoFacing(out.str());
return MoveNear(bot->GetMapId(), go->GetPositionX(), go->GetPositionY(), go->GetPositionZ() + 0.5f,
sPlayerbotAIConfig.followDistance);
@@ -103,7 +103,7 @@ bool GoAction::Execute(Event event)
if (strstri(unit->GetName().c_str(), param.c_str()))
{
std::ostringstream out;
out << "Двигаюсь к: " << unit->GetName();
out << "Moving to " << unit->GetName();
botAI->TellMasterNoFacing(out.str());
return MoveNear(bot->GetMapId(), unit->GetPositionX(), unit->GetPositionY(),
unit->GetPositionZ() + 0.5f, sPlayerbotAIConfig.followDistance);
@@ -137,7 +137,7 @@ bool GoAction::Execute(Event event)
out << x << ";" << y << ";" << z << " =";
out << "путь: ";
out << "path is: ";
out << type;
@@ -179,20 +179,20 @@ bool GoAction::Execute(Event event)
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, x, y),
sPlayerbotAIConfig.reactDistance))
{
botAI->TellMaster("Слишком далеко.");
botAI->TellMaster("It is too far away");
return false;
}
if (map->IsInWater(bot->GetPhaseMask(), x, y, z, bot->GetCollisionHeight()))
{
botAI->TellError("Цель находится в воде");
botAI->TellError("It is in water");
return false;
}
float ground = map->GetHeight(x, y, z + 0.5f);
if (ground <= INVALID_HEIGHT)
{
botAI->TellError("Я не могу туда пройти");
botAI->TellError("I can't go there");
return false;
}
@@ -200,7 +200,7 @@ bool GoAction::Execute(Event event)
Map2ZoneCoordinates(x1, y1, bot->GetZoneId());
std::ostringstream out;
out << "Двигаюсь к: " << x1 << "," << y1;
out << "Moving to " << x1 << "," << y1;
botAI->TellMasterNoFacing(out.str());
return MoveNear(bot->GetMapId(), x, y, z + 0.5f, sPlayerbotAIConfig.followDistance);
@@ -212,16 +212,16 @@ bool GoAction::Execute(Event event)
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, pos.x, pos.y),
sPlayerbotAIConfig.reactDistance))
{
botAI->TellError("Слишком далеко.");
botAI->TellError("It is too far away");
return false;
}
std::ostringstream out;
out << "Двигаюсь к позиции: " << param;
out << "Moving to position " << param;
botAI->TellMasterNoFacing(out.str());
return MoveNear(bot->GetMapId(), pos.x, pos.y, pos.z + 0.5f, sPlayerbotAIConfig.followDistance);
}
botAI->TellMaster("Напишите «go x,y», «go [объект]», «go unit» или «go position», и я отправлюсь туда");
botAI->TellMaster("Whisper 'go x,y', 'go [game object]', 'go unit' or 'go position' and I will go there");
return false;
}
@@ -83,7 +83,7 @@ bool GossipHelloAction::ProcessGossip(int32 menuToSelect, bool silent)
if (menuToSelect != -1 && !menu.GetItem(menuToSelect))
{
if (!silent)
botAI->TellError("Неизвестный вариант диалога");
botAI->TellError("Unknown gossip option");
return false;
}
@@ -138,7 +138,7 @@ bool GossipHelloAction::Execute(ObjectGuid guid, int32 menuToSelect, bool silent
else if (!bot->PlayerTalkClass)
{
if (!silent)
botAI->TellError("Сначала нужно поговорить");
botAI->TellError("I need to talk first");
return false;
}
else
@@ -28,17 +28,17 @@ bool GuildAcceptAction::Execute(Event event)
uint32 guildId = inviter->GetGuildId();
if (!guildId)
{
botAI->TellError("Вы не состоите в гильдии!");
botAI->TellError("You are not in a guild!");
accept = false;
}
else if (bot->GetGuildId())
{
botAI->TellError("Я уже состою в гильдии");
botAI->TellError("Sorry, I am in a guild already");
accept = false;
}
else if (!botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, inviter, true))
{
botAI->TellError("Я не хочу вступать в вашу гильдию :(");
botAI->TellError("Sorry, I don't want to join your guild :(");
accept = false;
}
@@ -17,7 +17,7 @@ bool GuildBankAction::Execute(Event event)
if (!bot->GetGuildId() || (GetMaster() && GetMaster()->GetGuildId() != bot->GetGuildId()))
{
botAI->TellMaster("Я не состою в вашей гильдии!");
botAI->TellMaster("I'm not in your guild!");
return false;
}
@@ -31,7 +31,7 @@ bool GuildBankAction::Execute(Event event)
return Execute(text, go);
}
botAI->TellMaster("Поблизости нет банка гильдии");
botAI->TellMaster("Cannot find the guild bank nearby");
return false;
}
@@ -65,11 +65,11 @@ bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* bank)
// check source pos rights (item moved to bank)
if (!guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM))
out << "Я не могу положить: " << chat->FormatItem(item->GetTemplate())
out << "I can't put " << chat->FormatItem(item->GetTemplate())
<< " to guild bank. I have no rights to put items in the first guild bank tab";
else
{
out << chat->FormatItem(item->GetTemplate()) << " положено в банк гильдии";
out << chat->FormatItem(item->GetTemplate()) << " put to guild bank";
guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, 0);
}
@@ -298,7 +298,7 @@ bool GuildLeaveAction::Execute(Event event)
Player* owner = event.getOwner();
if (owner && !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, owner, true))
{
botAI->TellError("Меня устраивает моя гильдия :)");
botAI->TellError("Sorry, I am happy in my guild :)");
return false;
}
@@ -23,7 +23,7 @@ bool HelpAction::Execute(Event /*event*/)
void HelpAction::TellChatCommands()
{
std::ostringstream out;
out << "Напишите одну из команд: ";
out << "Whisper any of: ";
out << CombineSupported(chatContext->supports());
out << ", [item], [quest] or [object] link";
botAI->TellError(out.str());
@@ -32,7 +32,7 @@ void HelpAction::TellChatCommands()
void HelpAction::TellStrategies()
{
std::ostringstream out;
out << "Доступные стратегии (команды co/nc/dead): ";
out << "Possible strategies (co/nc/dead commands): ";
out << CombineSupported(botAI->GetAiObjectContext()->GetSupportedStrategies());
botAI->TellError(out.str());
}
@@ -30,13 +30,13 @@ bool HireAction::Execute(Event /*event*/)
if (charCount >= 10)
{
botAI->TellMaster("У вас уже максимальное количество персонажей");
botAI->TellMaster("You already have the maximum number of characters");
return false;
}
if (bot->GetLevel() > master->GetLevel())
{
botAI->TellMaster("Нельзя нанять персонажа выше вашего уровня");
botAI->TellMaster("You cannot hire higher level characters than you");
return false;
}
@@ -46,13 +46,13 @@ bool HireAction::Execute(Event /*event*/)
if (discount < moneyReq)
{
std::ostringstream out;
out << "Вы не можете меня нанять: мы почти не знакомы. Требуется не менее " << chat->formatMoney(moneyReq)
out << "You cannot hire me - I barely know you. Make sure you have at least " << chat->formatMoney(moneyReq)
<< " as a trade discount";
botAI->TellMaster(out.str());
return false;
}
botAI->TellMaster("Я присоединюсь после вашего следующего входа в игру");
botAI->TellMaster("I will join you at your next relogin");
bot->SetMoney(moneyReq);
RandomPlayerbotMgr::instance().Remove(bot);
@@ -130,40 +130,40 @@ void InventoryAction::TellItems(std::map<uint32, uint32> itemMap, std::map<uint3
switch (proto->Class)
{
case ITEM_CLASS_CONSUMABLE:
botAI->TellMaster("--- Расходуемые предметы ---");
botAI->TellMaster("--- consumable ---");
break;
case ITEM_CLASS_CONTAINER:
botAI->TellMaster("--- Сумки ---");
botAI->TellMaster("--- container ---");
break;
case ITEM_CLASS_WEAPON:
botAI->TellMaster("--- Оружие ---");
botAI->TellMaster("--- weapon ---");
break;
case ITEM_CLASS_ARMOR:
botAI->TellMaster("--- Доспехи ---");
botAI->TellMaster("--- armor ---");
break;
case ITEM_CLASS_REAGENT:
botAI->TellMaster("--- Реагенты ---");
botAI->TellMaster("--- reagent ---");
break;
case ITEM_CLASS_PROJECTILE:
botAI->TellMaster("--- Боеприпасы ---");
botAI->TellMaster("--- projectile ---");
break;
case ITEM_CLASS_TRADE_GOODS:
botAI->TellMaster("--- Хозяйственные товары ---");
botAI->TellMaster("--- trade goods ---");
break;
case ITEM_CLASS_RECIPE:
botAI->TellMaster("--- Рецепты ---");
botAI->TellMaster("--- recipe ---");
break;
case ITEM_CLASS_QUIVER:
botAI->TellMaster("--- Колчаны ---");
botAI->TellMaster("--- quiver ---");
break;
case ITEM_CLASS_QUEST:
botAI->TellMaster("--- Предметы заданий ---");
botAI->TellMaster("--- quest items ---");
break;
case ITEM_CLASS_KEY:
botAI->TellMaster("--- Ключи ---");
botAI->TellMaster("--- keys ---");
break;
case ITEM_CLASS_MISC:
botAI->TellMaster("--- Прочее ---");
botAI->TellMaster("--- other ---");
break;
}
}
@@ -438,7 +438,7 @@ bool LfgAction::Execute(Event event)
std::ostringstream out;
if (allowedRoles[role] > 1)
{
out << "Присоединяюсь в роли: " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " "
out << "Joining as " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " "
<< placeholders["%role"] << " spots left.";
botAI->TellMasterNoFacing(out.str());
@@ -447,7 +447,7 @@ bool LfgAction::Execute(Event event)
}
else
{
out << "Присоединяюсь в роли: " << placeholders["%role"] << ".";
out << "Joining as " << placeholders["%role"] << ".";
botAI->TellMasterNoFacing(out.str());
//botAI->DoSpecificAction("autogear");
@@ -86,7 +86,7 @@ bool LeaveGroupAction::Leave()
Player* master = botAI -> GetMaster();
if (master)
botAI->TellMaster("До свидания!", PLAYERBOT_SECURITY_TALK);
botAI->TellMaster("Goodbye!", PLAYERBOT_SECURITY_TALK);
botAI->LeaveOrDisbandGroup();
return true;
@@ -44,19 +44,19 @@ void ListQuestsAction::ListQuests(QuestListFilter filter, QuestTravelDetail trav
bool showCompleted = filter & QUEST_LIST_FILTER_COMPLETED;
if (showIncompleted)
botAI->TellMaster("--- Невыполненные задания ---");
botAI->TellMaster("--- Incompleted quests ---");
uint32 incompleteCount = ListQuests(false, !showIncompleted, travelDetail);
if (showCompleted)
botAI->TellMaster("--- Выполненные задания ---");
botAI->TellMaster("--- Completed quests ---");
uint32 completeCount = ListQuests(true, !showCompleted, travelDetail);
botAI->TellMaster("--- Итого ---");
botAI->TellMaster("--- Summary ---");
std::ostringstream out;
out << "Всего: " << (completeCount + incompleteCount) << " / 25 (невыполнено: " << incompleteCount
out << "Total: " << (completeCount + incompleteCount) << " / 25 (incompleted: " << incompleteCount
<< ", completed: " << completeCount << ")";
botAI->TellMaster(out);
}
@@ -284,11 +284,11 @@ bool ListSpellsAction::Execute(Event event)
if (spells.empty())
{
// CHANGE: Give early feedback when no spells match the filter.
botAI->TellMaster("Заклинания не найдены.");
botAI->TellMaster("No spells found.");
return true;
}
botAI->TellMaster("=== Заклинания ===");
botAI->TellMaster("=== Spells ===");
std::sort(spells.begin(), spells.end(), CompareSpells);
@@ -17,11 +17,11 @@ bool LogLevelAction::Execute(Event event)
if (param != "?")
{
value->Set(string2logLevel(param));
out << "Уровень журнала установлен: " << logLevel2string(value->Get());
out << "My log level set to " << logLevel2string(value->Get());
}
else
{
out << "Текущий уровень журнала: " << logLevel2string(value->Get());
out << "My log level is " << logLevel2string(value->Get());
}
botAI->TellMaster(out);
@@ -24,14 +24,14 @@ bool LootStrategyAction::Execute(Event event)
{
{
std::ostringstream out;
out << "Стратегия добычи: ";
out << "Loot strategy: ";
out << lootStrategy->Get()->GetName();
botAI->TellMaster(out);
}
{
std::ostringstream out;
out << "Всегда собирать: ";
out << "Always loot items: ";
for (uint32 itemId : alwaysLootItems)
{
@@ -54,7 +54,7 @@ bool LootStrategyAction::Execute(Event event)
lootStrategy->Set(LootStrategyValue::instance(strategy));
std::ostringstream out;
out << "Установлена стратегия добычи: " << lootStrategy->Get()->GetName();
out << "Loot strategy set to " << lootStrategy->Get()->GetName();
botAI->TellMaster(out);
return true;
}
@@ -80,12 +80,12 @@ bool LootStrategyAction::Execute(Event event)
if (j != alwaysLootItems.end())
alwaysLootItems.erase(j);
botAI->TellMaster("Предметы удалены из списка обязательного сбора");
botAI->TellMaster("Item(s) removed from always loot list");
}
else
{
alwaysLootItems.insert(itemid);
botAI->TellMaster("Предметы добавлены в список обязательного сбора");
botAI->TellMaster("Item(s) added to always loot list");
}
}
}
@@ -17,7 +17,7 @@ class TellMailProcessor : public MailProcessor
public:
bool Before(PlayerbotAI* botAI) override
{
botAI->TellMaster("=== Почта ===");
botAI->TellMaster("=== Mailbox ===");
tells.clear();
return true;
}
@@ -83,7 +83,7 @@ public:
Player* bot = botAI->GetBot();
if (!CheckBagSpace(bot))
{
botAI->TellError("В сумках недостаточно места");
botAI->TellError("Not enough bag space");
return false;
}
@@ -256,7 +256,7 @@ bool MailAction::Execute(Event event)
if (!MailProcessor::FindMailbox(botAI))
{
botAI->TellError("Поблизости нет почтового ящика");
botAI->TellError("There is no mailbox nearby");
return false;
}
@@ -33,7 +33,7 @@ bool MoveToRpgTargetAction::Execute(Event /*event*/)
if (botAI->HasStrategy("debug rpg", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject())
{
std::ostringstream out;
out << "Направляюсь к цели: ";
out << "Heading to: ";
out << chat->FormatWorldobject(guidP.GetWorldObject());
botAI->TellMasterNoFacing(out);
}
@@ -55,9 +55,9 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/)
{
std::ostringstream out;
if (botAI->GetMaster() && !bot->GetGroup()->IsMember(botAI->GetMaster()->GetGUID()))
out << "Немного подожду: ";
out << "Waiting a bit for ";
else
out << "Пожалуйста, поторопитесь: ";
out << "Please hurry up ";
out << member->GetName();
@@ -164,7 +164,7 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
if (dest.isSet())
return MoveTo(dest.mapId, dest.x, dest.y, dest.z);
else
botAI->TellError("Ни один путь не находится в поле зрения");
botAI->TellError("All paths not in LOS");
return false;
}
@@ -1160,13 +1160,13 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
if (bot->isDead() && botAI->GetMaster()->IsAlive())
{
bot->ResurrectPlayer(1.0f, false);
botAI->TellMasterNoFacing("Я снова жив!");
botAI->TellMasterNoFacing("I live, again!");
}
else
botAI->TellError("Я застрял во время следования");
botAI->TellError("I am stuck while following");
bot->CombatStop(true);
botAI->TellMasterNoFacing("Я скоро буду там.");
botAI->TellMasterNoFacing("I will there soon.");
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(target->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(),
target->GetOrientation()); return false;
@@ -1379,7 +1379,7 @@ bool MovementAction::Flee(Unit* target)
if (!IsMovingAllowed())
{
botAI->TellError("Я застрял при отступлении");
botAI->TellError("I am stuck while fleeing");
return false;
}
@@ -1535,7 +1535,7 @@ bool MovementAction::Flee(Unit* target)
float rx, ry, rz;
if (!manager.CalculateDestination(&rx, &ry, &rz))
{
botAI->TellError("Некуда отступать");
botAI->TellError("Nowhere to flee");
return false;
}
@@ -1940,7 +1940,7 @@ bool AvoidAoeAction::AvoidAuraWithDynamicObj()
lastTellTimer = time(NULL);
lastMoveTimer = getMSTime();
std::ostringstream out;
out << "Избегаю: " << name.str() << " (" << spellInfo->Id << ")" << "; радиус " << radius << " — [аура]";
out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius << " - [Aura]";
bot->Say(out.str(), LANG_UNIVERSAL);
}
return true;
@@ -2008,7 +2008,7 @@ bool AvoidAoeAction::AvoidGameObjectWithDamage()
lastTellTimer = time(NULL);
lastMoveTimer = getMSTime();
std::ostringstream out;
out << "Избегаю: " << name.str() << " (" << spellInfo->Id << ")" << "; радиус " << radius
out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius
<< " - [Trap]";
bot->Say(out.str(), LANG_UNIVERSAL);
}
@@ -2075,7 +2075,7 @@ bool AvoidAoeAction::AvoidUnitWithDamageAura()
lastTellTimer = time(NULL);
lastMoveTimer = getMSTime();
std::ostringstream out;
out << "Избегаю: " << name.str() << " (" << triggerSpellInfo->Id << ")"
out << "I'm avoiding " << name.str() << " (" << triggerSpellInfo->Id << ")"
<< " Radius " << radius << " - [Unit Trigger]";
bot->Say(out.str(), LANG_UNIVERSAL);
}
@@ -2554,7 +2554,7 @@ bool DisperseSetAction::Execute(Event event)
if (text == "disable")
{
RESET_AI_VALUE(float, "disperse distance");
botAI->TellMasterNoFacing("Рассредоточение отключено");
botAI->TellMasterNoFacing("Disable disperse");
return true;
}
if (text == "enable" || text == "reset")
@@ -2569,7 +2569,7 @@ bool DisperseSetAction::Execute(Event event)
}
float dis = AI_VALUE(float, "disperse distance");
std::ostringstream out;
out << "Включено рассредоточение с дистанцией " << std::setprecision(2) << dis;
out << "Enable disperse distance " << std::setprecision(2) << dis;
botAI->TellMasterNoFacing(out.str());
return true;
}
@@ -2579,13 +2579,13 @@ bool DisperseSetAction::Execute(Event event)
std::ostringstream out;
if (dis <= 0.0f)
{
out << "Сначала включите рассредоточение";
out << "Enable disperse first";
botAI->TellMasterNoFacing(out.str());
return true;
}
dis += 1.0f;
SET_AI_VALUE(float, "disperse distance", dis);
out << "Дистанция рассредоточения увеличена до " << std::setprecision(2) << dis;
out << "Increase disperse distance to " << std::setprecision(2) << dis;
botAI->TellMasterNoFacing(out.str());
return true;
}
@@ -2599,7 +2599,7 @@ bool DisperseSetAction::Execute(Event event)
}
SET_AI_VALUE(float, "disperse distance", dis);
std::ostringstream out;
out << "Дистанция рассредоточения увеличена до " << std::setprecision(2) << dis;
out << "Increase disperse distance to " << std::setprecision(2) << dis;
botAI->TellMasterNoFacing(out.str());
return true;
}
@@ -2611,18 +2611,18 @@ bool DisperseSetAction::Execute(Event event)
std::ostringstream out;
if (dis < 0 || dis > 100.0f)
{
out << "Недопустимая дистанция рассредоточения: " << std::setprecision(2) << dis;
out << "Invalid disperse distance " << std::setprecision(2) << dis;
}
else
{
SET_AI_VALUE(float, "disperse distance", dis);
out << "Дистанция рассредоточения установлена: " << std::setprecision(2) << dis;
out << "Set disperse distance to " << std::setprecision(2) << dis;
}
botAI->TellMasterNoFacing(out.str());
return true;
}
std::ostringstream out;
out << "Использование: disperse [enable | disable | increase | decrease | set {дистанция}]";
out << "Usage: disperse [enable | disable | increase | decrease | set {distance}]";
float dis = AI_VALUE(float, "disperse distance");
if (dis > 0.0f)
{
@@ -36,6 +36,6 @@ void OpenItemAction::OpenItem(Item* item, uint8 bag, uint8 slot)
botAI->GetAiObjectContext()->GetValue<LootObject>("loot target")->Set(lootObject);
std::ostringstream out;
out << "Открыт предмет: " << item->GetTemplate()->Name1;
out << "Opened item: " << item->GetTemplate()->Name1;
botAI->TellMaster(out.str());
}
@@ -17,9 +17,9 @@ bool OutfitAction::Execute(Event event)
if (param == "?")
{
List();
botAI->TellMaster("outfit <имя> +[предмет] — добавить предметы");
botAI->TellMaster("outfit <имя> -[предмет] — удалить предметы");
botAI->TellMaster("outfit <имя> equip/replace — надеть предметы");
botAI->TellMaster("outfit <name> +[item] to add items");
botAI->TellMaster("outfit <name> -[item] to remove items");
botAI->TellMaster("outfit <name> equip/replace to equip items");
}
else
{
@@ -30,7 +30,7 @@ bool OutfitAction::Execute(Event event)
Save(name, items);
std::ostringstream out;
out << "Настраиваю комплект: " << name << " как " << param;
out << "Setting outfit " << name << " as " << param;
botAI->TellMaster(out);
return true;
}
@@ -48,7 +48,7 @@ bool OutfitAction::Execute(Event event)
if (command == "equip")
{
std::ostringstream out;
out << "Надеваю комплект: " << name;
out << "Equipping outfit " << name;
botAI->TellMaster(out);
EquipItems(outfit);
@@ -57,7 +57,7 @@ bool OutfitAction::Execute(Event event)
else if (command == "replace")
{
std::ostringstream out;
out << "Заменяю текущую экипировку комплектом: " << name;
out << "Replacing current equip with outfit " << name;
botAI->TellMaster(out);
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++)
@@ -82,7 +82,7 @@ bool OutfitAction::Execute(Event event)
else if (command == "reset")
{
std::ostringstream out;
out << "Сбрасываю комплект: " << name;
out << "Resetting outfit " << name;
botAI->TellMaster(out);
Save(name, ItemIds());
@@ -91,7 +91,7 @@ bool OutfitAction::Execute(Event event)
else if (command == "update")
{
std::ostringstream out;
out << "Обновляю комплект текущими предметами: " << name;
out << "Updating with current items outfit " << name;
botAI->TellMaster(out);
Update(name);
@@ -38,7 +38,7 @@ bool PetitionSignAction::Execute(Event event)
if (bot->GetArenaTeamId(slot))
{
// player is already in an arena team
botAI->TellError("У меня уже есть такая команда");
botAI->TellError("Sorry, I am already in such team");
accept = false;
}
}
@@ -46,13 +46,13 @@ bool PetitionSignAction::Execute(Event event)
{
if (bot->GetGuildId())
{
botAI->TellError("Я уже состою в гильдии");
botAI->TellError("Sorry, I am in a guild already");
accept = false;
}
if (bot->GetGuildIdInvited())
{
botAI->TellError("Меня уже пригласили в гильдию");
botAI->TellError("Sorry, I am invited to a guild already");
accept = false;
}
@@ -60,7 +60,7 @@ bool PetitionSignAction::Execute(Event event)
/*if (QueryResult* result = CharacterDatabase.Query("SELECT playerguid FROM petition_sign WHERE player_account =
{} AND petitionguid = {}'", bot->GetSession()->GetAccountId(), petitionGuid.GetCounter()))
{
botAI->TellError("Я уже подписал эту хартию");
botAI->TellError("Sorry, I already signed this pettition");
accept = false;
}
*/
@@ -88,7 +88,7 @@ bool PetitionSignAction::Execute(Event event)
WorldPacket data(CMSG_PETITION_SIGN, 20);
data << petitionGuid << unk;
bot->GetSession()->HandlePetitionSignOpcode(data);
bot->Say("Спасибо за приглашение!", LANG_UNIVERSAL);
bot->Say("Thanks for the invite!", LANG_UNIVERSAL);
LOG_INFO("playerbots", "Bot {} <{}> accepts {} invite", bot->GetGUID().ToString().c_str(),
bot->GetName().c_str(), isArena ? "Arena" : "Guild");
return true;
@@ -12,7 +12,7 @@
void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos)
{
std::ostringstream out;
out << "Позиция " << name;
out << "Position " << name;
if (pos.isSet())
{
@@ -22,7 +22,7 @@ void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos)
out << " is set to " << x << "," << y;
}
else
out << " не установлена";
out << " is not set";
botAI->TellMaster(out);
}
@@ -52,7 +52,7 @@ bool PositionAction::Execute(Event event)
std::vector<std::string> params = split(param, ' ');
if (params.size() != 2)
{
botAI->TellMaster("Напишите: position <имя> ?/set/reset");
botAI->TellMaster("Whisper position <name> ?/set/reset");
return false;
}
@@ -72,7 +72,7 @@ bool PositionAction::Execute(Event event)
posMap[name] = pos;
std::ostringstream out;
out << "Позиция " << name << " установлена";
out << "Position " << name << " is set";
botAI->TellMaster(out);
return true;
}
@@ -83,7 +83,7 @@ bool PositionAction::Execute(Event event)
posMap[name] = pos;
std::ostringstream out;
out << "Позиция " << name << " установлена";
out << "Position " << name << " is set";
botAI->TellMaster(out);
return true;
}
@@ -94,7 +94,7 @@ bool PositionAction::Execute(Event event)
posMap[name] = pos;
std::ostringstream out;
out << "Позиция " << name << " сброшена";
out << "Position " << name << " is reset";
botAI->TellMaster(out);
return true;
}
@@ -108,7 +108,7 @@ bool MoveToPositionAction::Execute(Event /*event*/)
if (!pos.isSet())
{
std::ostringstream out;
out << "Позиция " << qualifier << " не установлена";
out << "Position " << qualifier << " is not set";
botAI->TellMaster(out);
return false;
}
@@ -169,7 +169,7 @@ bool ReturnToStayPositionAction::isPossible()
const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z);
if (distance > sPlayerbotAIConfig.reactDistance)
{
botAI->TellMaster("Заданная точка слишком далеко. Останусь на текущем месте");
botAI->TellMaster("The stay position is too far to return. I am going to stay where I am now");
// Set the stay position to current position
stayPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId());
@@ -139,7 +139,7 @@ std::string const QueryItemUsageAction::QueryItemPrice(ItemTemplate const* item)
}
}
if (sellPrice)
msg << "Продажа: " << chat->formatMoney(sellPrice);
msg << "Sell: " << chat->formatMoney(sellPrice);
std::ostringstream out;
out << item->ItemId;
@@ -153,7 +153,7 @@ std::string const QueryItemUsageAction::QueryItemPrice(ItemTemplate const* item)
if (sellPrice)
msg << " ";
msg << "Покупка: " << chat->formatMoney(buyPrice);
msg << "Buy: " << chat->formatMoney(buyPrice);
}
return msg.str();
@@ -117,7 +117,7 @@ void QueryQuestAction::TellObjectives(uint32 questId)
// Checks if the questTemplate is valid
if (!questTemplate)
{
botAI->TellMaster("Шаблон задания не найден.");
botAI->TellMaster("Quest template not found.");
return;
}
@@ -158,9 +158,9 @@ bool QuestAction::CompleteQuest(Player* player, uint32 entry)
if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
LOG_INFO("playerbots", "{} => Quest [ {} ] completed", bot->GetName(), pQuest->GetTitle());
bot->Say("Задание [ " + text_quest + " ] completed", LANG_UNIVERSAL);
bot->Say("Quest [ " + text_quest + " ] completed", LANG_UNIVERSAL);
}
botAI->TellMasterNoFacing("Задание выполнено: " + text_quest);
botAI->TellMasterNoFacing("Quest completed " + text_quest);
player->CompleteQuest(entry);
@@ -188,7 +188,7 @@ bool QuestAction::ProcessQuests(WorldObject* questGiver)
{
//if (botAI->HasStrategy("debug", BotState::BOT_STATE_COMBAT) || botAI->HasStrategy("debug", BotState::BOT_STATE_NON_COMBAT))
botAI->TellError("Не удалось поговорить с персонажем, выдающим задание");
botAI->TellError("Cannot talk to quest giver");
return false;
}
@@ -220,18 +220,18 @@ bool QuestAction::AcceptQuest(Quest const* quest, ObjectGuid questGiver)
uint32 questId = quest->GetQuestId();
if (bot->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE)
out << "Уже выполнено";
out << "Already completed";
else if (!bot->CanTakeQuest(quest, false))
{
if (!bot->SatisfyQuestStatus(quest, false))
out << "Уже выполняется";
out << "Already on";
else
out << "Невозможно взять";
out << "Can't take";
}
else if (!bot->SatisfyQuestLog(false))
out << "Журнал заданий заполнен";
out << "Quest log is full";
else if (!bot->CanAddQuest(quest, false))
out << "Сумки заполнены";
out << "Bags are full";
else
{
WorldPacket p(CMSG_QUESTGIVER_ACCEPT_QUEST);
@@ -250,11 +250,11 @@ bool QuestAction::AcceptQuest(Quest const* quest, ObjectGuid questGiver)
if (bot->GetQuestStatus(questId) != QUEST_STATUS_NONE && bot->GetQuestStatus(questId) != QUEST_STATUS_REWARDED)
{
BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest);
out << "Принято: " << chat->FormatQuest(quest);
out << "Accepted " << chat->FormatQuest(quest);
botAI->TellMaster(out);
return true;
}
out << "Невозможно принять";
out << "Cannot accept";
}
out << " " << chat->FormatQuest(quest);
@@ -288,7 +288,7 @@ bool QuestUpdateCompleteAction::Execute(Event event)
// }
const auto format = ChatHelper::FormatQuest(qInfo);
if (botAI->GetMaster())
botAI->TellMasterNoFacing("Задание выполнено: " + format);
botAI->TellMasterNoFacing("Quest completed " + format);
BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, qInfo);
botAI->rpgStatistic.questCompleted++;
// LOG_DEBUG("playerbots", "[New rpg] {} complete quest {}", bot->GetName(), qInfo->GetQuestId());
@@ -461,7 +461,7 @@ bool QuestUpdateFailedTimerAction::Execute(Event event)
}
else
{
botAI->TellMaster("Не удалось запустить таймер задания: " + std::to_string(questId));
botAI->TellMaster("Failed timer for " + std::to_string(questId));
}
//drop quest
@@ -16,7 +16,7 @@ bool QuestConfirmAcceptAction::Execute(Event event)
return false;
}
std::ostringstream out;
out << "Задание: " << chat->FormatQuest(quest) << " — подтвердите принятие";
out << "Quest: " << chat->FormatQuest(quest) << " confirm accept";
botAI->TellMaster(out);
bot->GetSession()->HandleQuestConfirmAccept(sendPacket);
return true;
@@ -97,19 +97,19 @@ public:
{
if (!bot->GetUInt32Value(PLAYER_AMMO_ID))
{
botAI->TellError("Закончились боеприпасы!");
botAI->TellError("Out of ammo!");
return false;
}
if (!bot->GetPet())
{
botAI->TellError("Нет питомца!");
botAI->TellError("No pet!");
return false;
}
if (bot->GetPet()->GetHappinessState() == UNHAPPY)
{
botAI->TellError("Питомец недоволен!");
botAI->TellError("Pet is unhappy!");
return false;
}
}
@@ -22,7 +22,7 @@ bool ReleaseSpiritAction::Execute(Event event)
{
if (!bot->InBattleground())
{
botAI->TellMasterNoFacing("Мне не требуется воскрешение, подожду здесь");
botAI->TellMasterNoFacing("I am not dead, will wait here");
// -follow in bg is overwriten each tick with +follow
// +stay in bg causes stuttering effect as bot is cycled between +stay and +follow each tick
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
@@ -33,7 +33,7 @@ bool ReleaseSpiritAction::Execute(Event event)
if (bot->GetCorpse() && bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
{
botAI->TellMasterNoFacing("Я уже дух");
botAI->TellMasterNoFacing("I am already a spirit");
return false;
}
@@ -45,7 +45,7 @@ bool RepairAllAction::Execute(Event /*event*/)
if (totalCost > 0)
{
std::ostringstream out;
out << "Ремонт: " << chat->formatMoney(totalCost) << " (" << unit->GetName() << ")";
out << "Repair: " << chat->formatMoney(totalCost) << " (" << unit->GetName() << ")";
botAI->TellMasterNoFacing(out.str());
bot->PlayDistanceSound(1116);
@@ -56,6 +56,6 @@ bool RepairAllAction::Execute(Event /*event*/)
return true;
}
botAI->TellError("Не удалось найти персонажа для ремонта");
botAI->TellError("Cannot find any npc to repair at");
return false;
}
@@ -46,6 +46,6 @@ bool ResetAiAction::Execute(Event event)
}
PlayerbotRepository::instance().Reset(botAI);
botAI->ResetStrategies(false);
botAI->TellMaster("Настройки ИИ сброшены");
botAI->TellMaster("AI was reset to defaults");
return true;
}
@@ -62,18 +62,18 @@ bool RevealGatheringItemAction::Execute(Event /*event*/)
return false;
std::ostringstream msg;
msg << "Вижу: " << ChatHelper::FormatGameobject(go) << ". ";
msg << "I see a " << ChatHelper::FormatGameobject(go) << ". ";
switch (go->GetGoType())
{
case GAMEOBJECT_TYPE_CHEST:
msg << "Давайте посмотрим.";
msg << "Let's look at it.";
break;
case GAMEOBJECT_TYPE_FISHINGNODE:
msg << "Давайте немного порыбачим.";
msg << "Let's fish a bit.";
break;
default:
msg << "Подойти ближе?";
msg << "Should we go nearer?";
}
// everything is fine, do it
@@ -28,7 +28,7 @@ bool ReviveFromCorpseAction::Execute(Event event)
{
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
{
botAI->TellMasterNoFacing("С возвращением!");
botAI->TellMasterNoFacing("Welcome back!");
botAI->ChangeStrategy("+follow,-stay", BOT_STATE_NON_COMBAT);
return true;
}
@@ -297,7 +297,7 @@ bool SpiritHealerAction::Execute(Event /*event*/)
Corpse* corpse = bot->GetCorpse();
if (!corpse)
{
botAI->TellError("Я не дух");
botAI->TellError("I am not a spirit");
return false;
}
@@ -322,7 +322,7 @@ bool SpiritHealerAction::Execute(Event /*event*/)
bot->SpawnCorpseBones();
context->GetValue<Unit*>("current target")->Set(nullptr);
bot->SetTarget();
botAI->TellMaster("Здравствуйте.");
botAI->TellMaster("Hello");
if (dCount > 20)
context->GetValue<uint32>("death count")->Set(0);
@@ -39,7 +39,7 @@ bool RewardAction::Execute(Event event)
if (groupLeaderUnit && Reward(itemId, groupLeaderUnit))
return true;
botAI->TellError("Не удалось поговорить с персонажем, выдающим задание");
botAI->TellError("Cannot talk to quest giver");
return false;
}
@@ -374,7 +374,7 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/)
"You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " +
guidP.GetPlayer()->GetName() /*chat->FormatWorldobject(guidP.GetPlayer())*/ + ".");
else
bot->Say("Можно использовать: " + chat->FormatItem(item->GetTemplate()) + " better than me, " +
bot->Say("You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " +
player->GetName() /*chat->FormatWorldobject(player)*/ + ".",
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
@@ -389,7 +389,7 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/)
}
}
else
bot->Say("Начинаю обмен с" + chat->FormatWorldobject(player),
bot->Say("Start trade with" + chat->FormatWorldobject(player),
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
botAI->SetNextCheckDelay(sPlayerbotAIConfig.rpgDelay);
@@ -20,15 +20,15 @@ bool RTSCAction::Execute(Event event)
if (command != "reset" && !master->HasSpell(RTSC_MOVE_SPELL))
{
master->learnSpell(RTSC_MOVE_SPELL, false);
botAI->TellMasterNoFacing("Управление RTS включено.");
botAI->TellMasterNoFacing("Заклинание Aedm для управления перемещением изучено.");
botAI->TellMasterNoFacing("RTS control enabled.");
botAI->TellMasterNoFacing("Aedm (Awesome energetic do move) spell trained.");
}
else if (command == "reset")
{
if (master->HasSpell(RTSC_MOVE_SPELL))
{
master->removeSpell(RTSC_MOVE_SPELL, SPEC_MASK_ALL, false);
botAI->TellMasterNoFacing("Заклинание управления RTS удалено.");
botAI->TellMasterNoFacing("RTS control spell removed.");
}
RESET_AI_VALUE(bool, "RTSC selected");
@@ -123,7 +123,7 @@ bool RTSCAction::Execute(Event event)
if (command.find("show") != std::string::npos)
{
std::ostringstream out;
out << "сохранено: ";
out << "saved: ";
for (auto value : botAI->GetAiObjectContext()->GetValues())
if (value.find("RTSC saved location::") != std::string::npos)

Some files were not shown because too many files have changed in this diff Show More