# mod-taxi-builder

GM-инструмент для создания собственных точек полёта и маршрутов
прямо в игре на сервере MoonWell (AzerothCore 3.3.5a). Не требует
ручного редактирования DBC: все данные складываются в три таблицы
`acore_world`, а внешний CLI [moonwell-taxi](../../tools/moonwell-taxi/)
собирает из них `TaxiNodes.dbc`, `TaxiPath.dbc` и `TaxiPathNode.dbc`
для серверной `dbc/`-папки и клиентского патча.

Все команды требуют права `SEC_ADMINISTRATOR` (стандартный GM-уровень
3+).

---

## Содержание

- [Установка](#установка)
- [Полный сценарий: новый маршрут с нуля](#полный-сценарий-новый-маршрут-с-нуля)
- [Команды](#команды)
  - [`.taxi node`](#taxi-node)
  - [`.taxi path`](#taxi-path)
  - [`.taxi record`](#taxi-record)
  - [`.taxi markers`](#taxi-markers)
  - [`.taxi export dbc`](#taxi-export-dbc)
- [Лимит TaxiMask и распределение id](#лимит-taximask-и-распределение-id)
- [Правила валидации](#правила-валидации)
- [Прореживание точек: `moonwell-taxi thin`](#прореживание-точек-moonwell-taxi-thin)
- [Структура БД](#структура-бд)
- [Связь с CLI moonwell-taxi](#связь-с-cli-moonwell-taxi)
- [Известные ограничения](#известные-ограничения)
- [FAQ / типичные ошибки](#faq--типичные-ошибки)

---

## Установка

1. Клонируйте репозиторий ядра и убедитесь, что папка
   `modules/mod-taxi-builder/` присутствует.
2. Примените миграцию схемы. Самый простой способ — через
   [import-custom-sql.sh](../../import-custom-sql.sh):

   ```bash
   docker compose up -d ac-database
   bash import-custom-sql.sh
   ```

   В `acore_world` появятся три таблицы: `custom_taxi_nodes`,
   `custom_taxi_paths`, `custom_taxi_path_nodes`. Скрипт идемпотентен —
   повторный запуск пропустит уже созданные таблицы.

3. Соберите ядро со включенными модулями (значение по умолчанию):

   ```bash
   mkdir -p build && cd build
   cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server \
            -DCMAKE_BUILD_TYPE=RelWithDebInfo \
            -DSCRIPTS=static -DMODULES=static
   make -j$(nproc) && make install
   ```

4. Перезапустите worldserver. На старте появится строка о регистрации
   `taxi_builder_commandscript`.

5. (Один раз) поставьте `PyMySQL` для CLI:

   ```bash
   sudo apt install -y python3-pymysql
   ```

---

## Полный сценарий: новый маршрут с нуля

Допустим, GM хочет добавить кастомный маршрут «Златоземье → Штормград»
(вернее, «новая точка → существующий узел Штормграда `id=2`»).

```
# 1. Встаём ровно туда, где должна быть новая точка полёта.
.taxi node create "Златоземье"
# -> [taxi-builder] node created: id=441 name="Златоземье" map=0 ...

# 2. Создаём маршрут от новой точки к Штормграду (vanilla node id=2),
#    стоимость — 10 медяков.
.taxi path create 441 2 10
# -> [taxi-builder] path created: id=1979  441 (Златоземье) -> 2 (Stormwind) ...

# 3. Запускаем запись маршрута.
.taxi record start 1979
# -> [taxi-builder] recording started for path 1979 (existing points: 0)

# 4. Идём/летим по будущему маршруту. На каждой ключевой точке:
.taxi record point
# (или с комментарием — пригодится для последующего отчёта)
.taxi record point "взлёт над таверной"
.taxi record point
.taxi record point
# ...записываем 15-25 точек...

# Если оступились — откат последней:
.taxi record undo

# 5. Завершаем запись. Точки сохраняются.
.taxi record finish
# -> path 1979 now has 18 waypoints.

# 6. Проверяем маршрут на типовые проблемы.
.taxi path validate 1979
# -> Validation: 0 errors, 2 warnings — OK

# (Опционально) Расставить визуальные маркеры на всех точках, обойти их пешком:
.taxi markers spawn 1979
# ... осмотр ...
.taxi markers clear 1979

# 7. Из консоли (или из чата) запускаем экспорт DBC.
moonwell-taxi export-dbc

# 8. Кладём серверные DBC в папку сервера и перезапускаем worldserver.
# 9. Кладём клиентские DBC в патч-MPQ и перезапускаем клиент (или удаляем Cache).
# 10. На месте мастера полётов в Златоземье спавним NPC с npcflag=flight master
#     и фракцией Альянса/Орды (для MVP — стандартные команды AzerothCore .npc add).
# 11. Подходим к нему, выбираем путь, летим.
```

После шага 9 точка появится у мастеров полётов и игроки смогут летать
по записанному маршруту.

---

## Команды

### `.taxi node`

Управление точками полёта (записи в `custom_taxi_nodes`).

#### `.taxi node create "Name"`

Создаёт новую такситочку из текущей позиции GM. Работает только из
игры (нужны координаты).

- `id` подбирается автоматически из безопасного диапазона (`>= 441`,
  избегая конфликтов с vanilla TaxiNodes ~313).
- `mount_alliance` и `mount_horde` определяются автоматически: модуль
  смотрит, какие mount-creature чаще всего используются у vanilla точек
  на текущей карте, и подставляет их. Если карта неизвестна — fallback
  `541/541` (как в примере ТЗ).
- Имя сохраняется как одна строка и при экспорте в DBC дублируется во
  все 16 локалей (отображается на любом клиенте).

```
.taxi node create "Златоземье"
[taxi-builder] node created: id=441 name="Златоземье" map=0
                xyz=(-9445.77, 79.40, 57.38) mounts=A:1572/H:295
```

#### `.taxi node list [filter]`

Печатает список точек: сначала custom (`custom_taxi_nodes`), затем
vanilla (`sTaxiNodesStore`). Без аргумента дамп ограничен 50 строками
(в чате не должно быть спама). С `filter` — подстрочный поиск без
учёта регистра. Доступна и из консоли.

```
.taxi node list Storm
[vanilla] 2 Stormwind, Elwynn @ map 0 (-9446.5, 64.4, 56.0)
[vanilla] 38 Stonewatch Tower, Redridge @ map 0 (-9241.3, -2280.7, 65.0)
Shown 2 (cap 50). Custom: 1, Vanilla: 365.
```

#### `.taxi node free`

Печатает все свободные TaxiNode id в диапазоне `1..448`, сгруппированные
в непрерывные подряд-идущие отрезки. Полезно перед `.taxi node create`
посмотреть, сколько слотов осталось, и до создания убедиться, что
auto-allocator не упрётся. Подробности — в разделе
[Лимит TaxiMask и распределение id](#лимит-taximask-и-распределение-id).

```text
.taxi node free
Free TaxiNode ids in 1..448: 83 total (7 above-vanilla 441..448, 76 in vanilla gaps).
  338..339 (2)
  ...
  441..448 (8)
```

#### `.taxi node goto <nodeId>`

Телепортирует GM к такситочке (custom или vanilla — что найдётся
первым). Удобно для быстрой проверки, что точка стоит, где надо.

```text
.taxi node goto 441
[taxi-builder] teleported to custom node 441 (Златоземье)
```

---

### `.taxi path`

Управление маршрутами (записи в `custom_taxi_paths`).

#### `.taxi path create <fromNodeId> <toNodeId> <cost>`

Создаёт направленный маршрут между двумя taxi-нодами. Любой конец
может быть как custom-нодом, так и vanilla — модуль ищет по обоим
источникам и валит, если ID нигде нет. Если уже есть включённый путь
с тем же `from→to` — отказ (нельзя случайно создать дубль).

- `id` подбирается автоматически из диапазона `>= 1979`.
- `cost` хранится в медяках.
- `is_enabled` ставится в `1`.

```
.taxi path create 441 2 10
[taxi-builder] path created: id=1979  441 (Златоземье) -> 2 (Stormwind)  cost=10 copper
Now record waypoints with: .taxi record start 1979  ->  .taxi record point ...  ->  .taxi record finish
```

#### `.taxi path reverse <pathId>`

Создаёт обратный путь из существующего:

1. Делает новый `TaxiPath` `to → from` с тем же `cost`.
2. Копирует все `TaxiPathNode` исходного пути в обратном порядке,
   пересчитывая `node_index` от `0`.
3. Печатает предупреждение из ТЗ: ручную проверку посадки никто не
   отменял.

```
.taxi path reverse 1979
[taxi-builder] reversed path 1979 (441->2) into new path 1980 (2->441) with 18 waypoints.
WARNING: reverse paths are auto-generated. Check landing waypoints by hand
         (`.taxi path goto 1980 <lastIndex>`)...
```

#### `.taxi path list`

Все custom-маршруты с резолвом имён узлов и счётчиком точек.

```
.taxi path list
1979  441 (Златоземье) -> 2 (Stormwind, Elwynn)  cost=10  pts=18  enabled
1980  2 (Stormwind, Elwynn) -> 441 (Златоземье)  cost=10  pts=18  enabled
Total: 2 custom paths.
```

#### `.taxi path report <pathId>`

Подробный отчёт по маршруту в формате секции 5.3 ТЗ.

```
.taxi path report 1979
PathID: 1979  (enabled)
From: 441 Златоземье [custom]
To: 2 Stormwind, Elwynn [vanilla]
Cost: 10 copper
Points: 18
Total distance: 1842.3 yd
Min segment: 41.7 yd  Max segment: 95.4 yd
Last point distance to ToTaxiNode: 4.8 yd
```

#### `.taxi path validate <pathId>`

Применяет правила секции 4 ТЗ. См. подробности в разделе
[Правила валидации](#правила-валидации).

```
.taxi path validate 1979
[warn]    segment 7->8 is 38.2 yd (< 40 yd, may be filtered by IsNodeIncludedInShortenedPath)
[warn]    sharp turn at NodeIndex 12: 89 deg (> 75). Consider adding intermediate points.
Validation: 0 errors, 2 warnings — OK
```

При наличии errors статус — `FAIL`. Маршрут с errors не пропустит
`.taxi path test`.

#### `.taxi path goto <pathId> <nodeIndex>`

Телепорт к конкретной точке маршрута. Удобно для шага «обход маркеров».

```
.taxi path goto 1979 7
[taxi-builder] teleported to path 1979, NodeIndex 7  map=0 (-9095.00, 230.00, 180.00)
```

#### `.taxi path test <pathId>`

«Сухой запуск»: запускает валидацию (отказ при errors), затем
телепортирует GM на первую точку маршрута, развернув лицом ко второй.
Реальный полёт по таксиспрайнам в этой версии возможен только после
прогона `moonwell-taxi export-dbc` и перезапуска worldserver+клиента.

```
.taxi path test 1979
  [warn] segment 7->8 is 38.2 yd ...
[taxi-builder] dropped at first waypoint of path 1979 (18 total).
Step through with `.taxi path goto 1979 <index>`.
Real taxi flight needs `moonwell-taxi export-dbc` + worldserver/client restart.
```

---

### `.taxi record`

Запись точек маршрута. Запись — *сессия в памяти*, привязанная к GM-у:
рестарт worldserver очистит сессию, но уже записанные waypoint-ы в БД
останутся, и сессию можно продолжить через `.taxi record start <pathId>`.

#### `.taxi record start <pathId>`

Начинает запись для существующего custom-пути. Запоминает baseline —
число точек, которые были у пути до старта (для корректного `cancel`).

```
.taxi record start 1979
[taxi-builder] recording started for path 1979 (existing points: 0).
Use `.taxi record point ["comment"]` and `.taxi record finish` when done.
```

#### `.taxi record point [comment]`

Записывает следующую точку с координатами и map_id GM-а. `node_index`
автоматически = `MAX(node_index) + 1` (или 0 для первой).

Опциональный комментарий пишется в `comment` колонку (до 255 символов).

```
.taxi record point
[taxi-builder] path=1979  point #0  map=0  (-9445.77, 79.40, 57.38)

.taxi record point "разворот над озером"
[taxi-builder] path=1979  point #1  map=0  (-9395.00, 80.00, 70.00)  // разворот над озером
```

#### `.taxi record undo`

Удаляет последнюю записанную в этой сессии точку. Не даёт уйти ниже
baseline (точки, существовавшие до `start`, защищены).

```
.taxi record undo
[taxi-builder] removed point #17 from path 1979.
```

#### `.taxi record finish`

Закрывает сессию. Точки остаются в БД. Печатает счётчики.

```
.taxi record finish
[taxi-builder] recording finished. Path 1979 now has 18 waypoints (added 18 this session).
Run `.taxi path report 1979` or `.taxi path validate 1979` to review.
```

#### `.taxi record cancel`

Откатывает текущую сессию: удаляет ровно те точки, которые добавил
этот `start ... point ...`. Существовавшие до сессии waypoint-ы не
трогаются — это полезно при правке уже готового маршрута.

```
.taxi record cancel
[taxi-builder] recording cancelled for path 1979; rolled back to 5 waypoints.
```

---

### `.taxi markers`

Временные визуальные маркеры на waypoint-ах. По умолчанию используется
GO `178559` (Banner Stand of Argent Crusade) — он мелкий, виден всем
и есть в стоковой `gameobject_template`. При желании смените
`MARKER_GO_ENTRY` в [src/cs_taxi_builder.cpp](src/cs_taxi_builder.cpp).

#### `.taxi markers spawn <pathId>`

Спавнит маркеры на всех точках маршрута, лежащих на текущей карте
GM-а. Точки на других континентах пропускаются (грид всё равно не
загружен — было бы бесполезно).

Маркеры держатся 24 часа или до явного `clear`. Повторный `spawn`
сначала чистит предыдущие, чтобы не плодить дубли.

```
.taxi markers spawn 1979
[taxi-builder] spawned 18 markers for path 1979 on map 0 (0 skipped on other maps).
                Auto-despawn in 86400 s. Use `.taxi markers clear 1979` to remove now.
```

#### `.taxi markers clear <pathId>`

Удаляет все маркеры данного пути.

```
.taxi markers clear 1979
[taxi-builder] cleared 18 markers for path 1979.
```

---

### `.taxi export dbc`

Внутри игры команда не делает экспорт сама — она лишь напоминает
запустить внешний CLI. Это сделано специально: генерация DBC, sha256
и копирование файлов — задача внешнего инструмента, чтобы не нагружать
runtime worldserver-а.

```
.taxi export dbc
[taxi-builder] export dbc stub: run `moonwell-taxi export-dbc` from CLI
```

См. раздел [Связь с CLI moonwell-taxi](#связь-с-cli-moonwell-taxi).

---

## Лимит TaxiMask и распределение id

`TaxiNode.id` ограничен сверху значением **448**. Это hardcoded в
`src/server/shared/DataStores/DBCStructure.h:2284`:

```cpp
static constexpr std::size_t TaxiMaskSize = 14;
typedef std::array<uint32, TaxiMaskSize> TaxiMask;
```

14 × 32 = 448 бит. По одному биту на каждую такситочку: «доступна
ли в taxi-сети» и «открыл ли её игрок». Сервер в
`ObjectMgr::GetNearestTaxiNode` пропускает любой узел с
`(id - 1) / 32 >= 14`, то есть `id > 448`.

Симптом нарушения: GM создал точку `id = 441`, custom NPC появился,
но при выборе destination сервер берёт ближайший **vanilla** узел
вместо нашего, строит другую трассу — игрок «крутится по дороге»
и в конце сваливается. Видимых ошибок в логе нет.

Поднять потолок без патча клиента нельзя: пакет `SMSG_SHOWTAXINODES`
шлёт ровно 14 uint32, клиент 3.3.5a читает фиксированный размер.

### Что внутри 1..448

В реальной vanilla-DBC проекта используется до `id = 440`, и часть
номеров в этом диапазоне пустует. Вместе с диапазоном выше vanilla
(`441..448`) даёт ~84 свободных слота. Команда `.taxi node free`
показывает текущую картину:

```text
.taxi node free
Free TaxiNode ids in 1..448: 84 total (8 above-vanilla 441..448, 76 in vanilla gaps).
  338..339 (2)
  343..347 (5)
  349..351 (3)
  354..356 (3)
  360..374 (15)
  378
  385..391 (7)
  396..403 (8)
  406..437 (32)
  441..448 (8)
```

`.taxi node create` использует двухзонный авто-выбор:

1. Сначала ищет первый свободный id в `441..448` (выше vanilla,
   всегда безопасно).
2. Если эта зона исчерпана — берёт первый свободный id в дырах
   `1..440`.

«Свободный» означает: id не загружен в `sTaxiNodesStore`
(merged DBC на сервере) **и** не присутствует в `custom_taxi_nodes`
(может быть ещё не задеплоен в DBC).

### Что делать при исчерпании всех 84 слотов

- Освободить слот: удалить лишнюю строку из `custom_taxi_nodes`
  (плюс зависимости в `custom_taxi_paths`).
- Перейти на бинарный патч `Wow.exe` под расширенный `TaxiMaskSize`
  (Cataclysm-эра приватки делают так — увеличивают до 32 uint32 =
  1024 ноды). Это вне scope модуля: нужен патч exe + изменение
  `TaxiMaskSize` в ядре + раскатка патча игрокам.
- Использовать «фейковые» полёты через скриптового NPC и `MoveSplinePath`,
  без записи в TaxiMask. Не показывается на карте как обычная точка
  полёта, но id неограничены. Тоже отдельный модуль.

---

## Правила валидации

`.taxi path validate <pathId>` применяет следующие правила
(пороги — константы в [src/cs_taxi_builder.cpp](src/cs_taxi_builder.cpp),
секция «Thresholds per spec section 4»):

| # | Правило | Уровень |
| --- | --- | --- |
| 4.1 | `from`/`to`-нод существует (custom или vanilla) | error |
| 4.1 | у пути есть хотя бы одна waypoint | error |
| 4.1 | `node_index` идёт подряд от 0 | error |
| 4.2 | минимум 3 точки | error |
| 4.2 | рекомендация: 12+ точек | warning |
| 4.3 | сегмент `> 200 yd` (XY) | error |
| 4.3 | сегмент `> 120 yd` (XY) | warning |
| 4.3 | сегмент `< 40 yd` (XY) | warning (`IsNodeIncludedInShortenedPath` режет) |
| 4.3 | сегмент пересекает границу карты | error (полёт между континентами невозможен) |
| 4.4 | первая точка `> 20 yd` от `FromTaxiNode` | warning |
| 4.4 | последняя точка `> 20 yd` от `ToTaxiNode` | warning |
| 4.4 | первая/последняя точка на другой карте, чем нод | error |
| 4.5 | модуль абсолютной разницы Z последней точки и `ToTaxiNode > 30 yd` | warning (риск падения) |
| 4.6 | поворот `> 75°` в плоскости XY между сегментами | warning |

`.taxi path test` отказывается работать, если есть хотя бы один error.
Warnings — индикативны, не блокируют.

---

## Прореживание точек: `moonwell-taxi thin`

Самая частая поломка свежезаписанного маршрута: в полёте таксИ
«крутится по дороге» и сбрасывает игрока на кладбище. Причина —
большинство сегментов короче 40 yd. AzerothCore при построении
spline-а вызывает `IsNodeIncludedInShortenedPath` (`SKIP_SPLINE_POINT_DISTANCE_SQ = 40 * 40`)
и выкидывает waypoint-ы, которые ближе ~40 yd друг к другу. Если у
вас 68 точек, среднее расстояние 12 yd — после фильтрации остаётся
3-5 точек, по которым spline крутится как угодно.

Симптом виден прямо в `.taxi path validate <pathId>`:

```
[warn]    segment 19->20 is 12.1 yd (< 40 yd, may be filtered by IsNodeIncludedInShortenedPath)
[warn]    segment 20->21 is 12.7 yd (< 40 yd, ...)
...
Validation: 0 errors, 67 warnings — OK
```

Чинить руками удалением точек по одной — больно. Внешний CLI имеет
подкоманду `thin`, которая делает это безопасно: пишет бэкап,
удаляет старые waypoint-ы и записывает прореженные с пересчитанным
`node_index` от 0.

```bash
# 1. посмотреть, что получится, без записи в БД
python3 tools/moonwell-taxi/moonwell_taxi.py thin --path 1979 --min-yd 50 --dry-run

# 2. реально применить
python3 tools/moonwell-taxi/moonwell_taxi.py thin --path 1979 --min-yd 50
```

Параметры:

- `--path <id>` — обязательный, какой `custom_taxi_paths.id` чистить.
- `--min-yd <float>` — минимальное XY-расстояние между соседними
  оставленными точками. Дефолт `50`. Для длинных прямых смело
  ставьте `60-80`, для извилистых — `40-50`.
- `--dry-run` — показать план без модификации БД.

Алгоритм:

1. Первая waypoint всегда сохраняется.
2. Каждая последующая сравнивается с последней оставленной по XY.
   Если ≥ `--min-yd` — сохраняется, иначе пропускается.
3. Последняя waypoint всегда сохраняется (анкер посадки).
4. Если предпоследняя получилась слишком близко к последней — её
   тоже выкидывают. Так не возникает «маленький хвост» на финише.
5. Все ID `node_index` пересчитываются от `0` без пропусков.
6. Перед удалением исходные строки копируются в
   `custom_taxi_path_nodes_backup_<pathId>_<unix_ts>` — откатить
   можно прямым SQL (команда сама печатает rollback в конце).

Пример вывода на боевом маршруте:

```
Path 1979: 68 -> 13 waypoints (min_yd=50.0, removed 55)
Backup table: `custom_taxi_path_nodes_backup_1979_1778283612`
To rollback:
  DELETE FROM custom_taxi_path_nodes WHERE path_id = 1979;
  INSERT INTO custom_taxi_path_nodes
    SELECT * FROM `custom_taxi_path_nodes_backup_1979_1778283612`;
```

Что делать после `thin`:

```
.taxi path validate 1979   # должно быть 0 errors, 0 warnings про <40 yd
.taxi path report   1979   # Min/Max segment в коридоре 40-120 yd
```

Затем `bash tools/moonwell-taxi/redeploy.sh` — экспорт DBC, заливка
в `var/client-data-live/dbc/`, рестарт worldserver-а.

**Важно:** `thin` работает только с записями в `custom_taxi_path_nodes`.
DBC сами по себе не меняются — для актуализации сервера нужен
`redeploy.sh` (или ручной `export-dbc` + `cp` + `restart`). Без этого
in-game проверка увидит старый, плотный маршрут.

---

## Структура БД

Все три таблицы в `acore_world`. См.
[data/sql/updates/pending_db_world/rev_*.sql](../../data/sql/updates/pending_db_world/).

```
custom_taxi_nodes
  id           int unsigned   PRIMARY KEY    (модуль выдаёт >= 441)
  name         varchar(100)                  (utf8mb4, утаивается во все 16 локалей DBC)
  map_id       smallint unsigned
  position_x   float
  position_y   float
  position_z   float
  mount_alliance int unsigned
  mount_horde    int unsigned
  created_by   bigint unsigned   (GUID GM-а на момент создания)
  created_at   timestamp
  updated_at   timestamp

custom_taxi_paths
  id                   int unsigned   PRIMARY KEY  (модуль выдаёт >= 1979)
  from_taxi_node_id    int unsigned                (custom ИЛИ vanilla)
  to_taxi_node_id      int unsigned                (custom ИЛИ vanilla)
  cost                 int unsigned                (медяки)
  is_enabled           tinyint(1)
  created_by, created_at, updated_at
  KEY idx_from_to (from_taxi_node_id, to_taxi_node_id)

custom_taxi_path_nodes
  id                   int unsigned   AUTO_INCREMENT  (внутренний; DBC ID — на экспорте)
  path_id              int unsigned
  node_index           int unsigned                  (UNIQUE с path_id, 0-based)
  map_id               smallint unsigned
  position_x, _y, _z   float
  flags                int unsigned                  (TaxiPathNode flags: teleport=1, stop=2 ...)
  delay                int unsigned
  arrival_event_id     int unsigned
  departure_event_id   int unsigned
  comment              varchar(255)
  created_at           timestamp
```

ID-диапазоны: модуль предпочитает `441..448` для новых нод, а когда
этот диапазон занят, использует свободные дырки внутри `1..440`.
Для путей модуль начинает с `1979`, сразу после vanilla max `1978`.
Схема БД эти правила не enforce-ит, поэтому в теории можно вставить ID
руками, но дальше CLI откажется писать DBC при коллизии с vanilla.

`from`/`to` в `custom_taxi_paths` намеренно без FK: маршрут регулярно
ведёт от custom-нода к vanilla (как в исходном кейсе ТЗ —
«Златоземье → Штормград»).

---

## Связь с CLI moonwell-taxi

Модуль *только* пишет в БД. DBC генерирует внешний инструмент
[tools/moonwell-taxi/](../../tools/moonwell-taxi/):

```bash
# Прочесть custom_taxi_*, смерджить с vanilla DBC из корня репозитория,
# записать оба комплекта (server + client) и проверить sha256.
python3 tools/moonwell-taxi/moonwell_taxi.py export-dbc

# Output:
#   build/taxi/export/server/dbc/TaxiNodes.dbc
#   build/taxi/export/server/dbc/TaxiPath.dbc
#   build/taxi/export/server/dbc/TaxiPathNode.dbc
#   build/taxi/export/client/DBFilesClient/TaxiNodes.dbc
#   build/taxi/export/client/DBFilesClient/TaxiPath.dbc
#   build/taxi/export/client/DBFilesClient/TaxiPathNode.dbc
#   + sha256.txt в каждом каталоге
```

Если sha256 серверной и клиентской копий не совпадают — CLI выходит с
`exit code 1`, не считая экспорт валидным.

**Важно:** модуль и CLI общаются *только через схему БД*. Никаких
общих хедеров, FIFO или RPC. Меняется схема — обновляется и CLI.

---

## Известные ограничения

- **Только custom_taxi_*, не модифицирует vanilla.** Чтобы поправить
  существующую такситочку или путь vanilla, нужны другие средства.
- **Имя пишется в locale slot текущего vanilla DBC.** Exporter
  авто-инферит slot и `Name_lang_mask` из `TaxiNodes.dbc` (для ruRU это
  slot `8`). Per-locale override таблицы пока нет.
- **Mount creature ID — авто-подбор по большинству на карте.** Для
  Outland/Northrend и нестандартных карт fallback `541/541` может
  выглядеть как Альянсный грифон у Орды. Меняется UPDATE-ом
  `custom_taxi_nodes.mount_horde`.
- **`.taxi markers spawn` спавнит только на текущей карте.** Точки на
  других континентах пропускаются — таково ограничение grid-load.
- **`.taxi path test` не запускает реальный flight.** Реальный полёт
  работает только через DBC; у `path test` MVP-уровень — телепорт на
  первую точку. Делать `Player::ActivateTaxiPathTo` для путей не из
  DBC требует более глубокой интеграции (out of scope MVP).
- **Сессия записи не персистится.** Рестарт worldserver сбрасывает
  состояние `.taxi record`. Workaround: `.taxi record start <pathId>`
  заново — точки в БД остаются.
- **Конвенция Alliance/Horde в DBC обратна тексту ТЗ.** В реальном
  DBC `MountCreatureID[0]` — Horde, `[1]` — Alliance
  (см. `ObjectMgr.cpp:7229`). Колонки `mount_alliance` /
  `mount_horde` в БД названы корректно — конверсия делается при записи
  в DBC.

---

## FAQ / типичные ошибки

### «Path 1979 has no waypoints — record some first»

Создан путь, но не записано ни одной точки. Запусти
`.taxi record start 1979`, пройди маршрут, нажимая
`.taxi record point` в нужных местах, заверши `.taxi record finish`.

### «custom_taxi_paths id=N not found»

Путь не существует или `is_enabled=0`. Список — `.taxi path list`.

### «from-node N does not exist (custom or vanilla)»

ID не найден ни в `custom_taxi_nodes`, ни в vanilla `TaxiNodes.dbc`.
Список — `.taxi node list <substring>`.

### «An enabled path X -> Y already exists with id Z»

Активный маршрут `from→to` уже существует. Либо
`UPDATE custom_taxi_paths SET is_enabled=0 WHERE id=Z;`, либо
используй существующий маршрут.

### `.taxi path validate` показывает много `< 40 yd` warnings

Сегменты слишком короткие — `IsNodeIncludedInShortenedPath` их режет
и реальный полёт пройдёт «через» эти точки. Удали лишние через
`.taxi record undo` или прямым `DELETE` по `node_index`.

### Marker GO entry 178559 is missing from gameobject_template

На сервере зачищен banner GO. Поменяй `MARKER_GO_ENTRY` в
[src/cs_taxi_builder.cpp](src/cs_taxi_builder.cpp) на любой видимый
GO из `gameobject_template` и пересобери модуль.

### После `export-dbc` сервер видит точку, а клиент — нет

Клиент читает свои `DBFilesClient/*.dbc` из MPQ-патчей. Проверь, что
файлы из `build/taxi/export/client/DBFilesClient/` попали в
подключённый патч (например, `patch-X.MPQ` с приоритетом выше
базовых) и удалён клиентский `Cache/`.

### `Server/client DBC match: FAIL` после экспорта

Не должно случаться при штатной работе CLI (server и client пишутся
из одного буфера). Если всё-таки — баг в инструменте, открывайте
issue с `sha256.txt` обоих комплектов.
