# Оптимизация обработки мобов (mob AI) — снижение ЦПУ мап-сервера Контекст: профиль показал, что игровой поток мап-сервера упирается в обработку мобов (`mob_ai_hard` / `db_obj_vforeach`). Целевой режим — `mob_ai = 0x20` (тестировщики подтвердили: поведение мобов лучше дефолтного). В этом режиме **полный** `mob_ai_sub_hard` гоняется по **всем** мобам карт с игроками каждые 100 мс (mob.c: `mob_ai_sub_lazy` → `if (battle_config.mob_ai&0x20 && users>0) return mob_ai_sub_hard(...)`). Все патчи сохраняют поведение режима `0x20` (bit-exact), меняется только стоимость. RAM-за-ЦПУ разрешён (кеши/буферы). --- ## Опт-1 — плоский массив живых мобов вместо DBMap (`livemob.c`) ### Что было `map_foreachmob()` итерировала `livemob_db` — DBMap (хеш-таблица) — через `db_obj_vforeach`. У всех DBMap `HASH_SIZE = 16381` (db.c:107), и `vforeach` **проходит все 16381 бакетов на каждом вызове**, плюс `va_copy` и обход дерева на каждый живой узел. При `mob_ai&0x20`: - hard-таймер (100 мс): 10 × 16381 ≈ **164 000 чтений пустых бакетов/сек**; - lazy-таймер (1000 мс): ещё 16381/сек. Эти накладные **фиксированы** и не зависят от числа мобов. ### Что стало `livemob_db` заменён плоским непрерывным массивом указателей `mob_data*` (`src/map/livemob.c`), который `map_foreachmob()` обходит напрямую: O(числа живых мобов), без хеш-бакетов и без `va_copy`-на-узел. - Каждый моб кеширует свой слот в `mob_data.livemob_idx` → удаление O(1) (swap с последним элементом). - Ведётся в тех же точках, что и раньше (`map_addiddb`/`map_deliddb`). - **Безопасность при гибели мобов во время прохода:** проход идёт по *снимку* массива под `map_freeblock_lock()` (освобождение `mob_data` откладывается до конца прохода — см. `unit_free`/`map_freeblock`), а моб, удалённый из индекса посреди прохода, пропускается по `livemob_idx < 0` (точный аналог `node->deleted` старого DBMap). Добавленные во время прохода мобы обрабатываются со следующего тика (как и раньше; у них свежий `last_thinktime`). RAM: один указатель на живого моба + равный по размеру снимок (десятки КБ). ### Тест `src/map/test_livemob.c` — standalone, не часть `make sql`: ``` gcc -DLIVEMOB_TEST -I src/map -o /tmp/test_livemob src/map/livemob.c src/map/test_livemob.c /tmp/test_livemob ``` Покрывает: добавление/обход, swap-remove середины/конца/начала, удаление не-индексированного (no-op), удаление мобов посреди прохода (включая текущего), баланс `freeblock`-лока. Прогон с `-fsanitize=address,undefined` — чисто. ### Проверка - Unit-тест — PASS, ASan/UBSan — чисто. - `make sql` — чисто, без новых предупреждений. - Boot-smoke — без крашей, «No memory leaks found» (init/final сбалансированы). ### Что проверить тестировщикам (на кластере, режим `mob_ai: 0x20`) - Мобы спавнятся, агрятся, преследуют и теряют цель как раньше; рандом-walk вне зоны игроков работает; слуги следуют за хозяином. - Массовая гибель мобов (AoE: Storm Gust, Meteor, Sharp Shooting) — без крашей. - Призываемые мобы (алхимик/SG), MVP-слуги — без аномалий. - Профиль под нагрузкой: доля `db_obj_vforeach`/`map_foreachmob` упала. --- ## Опт-2 — не гонять дублирующий 1000мс lazy-таймер при `mob_ai&0x20` ### Что было Зарегистрированы два таймера (mob.c): `mob_ai_hard` каждые 100 мс и `mob_ai_lazy` каждые 1000 мс. При `mob_ai&0x20` **оба** вызывают один и тот же `map_foreachmob(mob_ai_sub_lazy_wrapper)`. ### Что стало `mob_ai_lazy` при `mob_ai&0x20` сразу возвращает 0. Обоснование эквивалентности: 100 мс hard-таймер уже обходит **всех** мобов через тот же путь — - карты с игроками: `mob_ai_sub_hard` (сам ограничен 100 мс по `last_thinktime`); - пустые карты: lazy-путь, ограниченный `DIFF_TICK < 10*MIN_MOBTHINKTIME` (1000 мс). Каденс lazy-действий задаёт **этот гейт**, а не период таймера, поэтому отдельный 1000 мс проход лишь отсекался бы гейтом. Убираем один полный проход по всем мобам в секунду без изменения поведения. (При выключенном `0x20` lazy-таймер работает как раньше — он там единственный обработчик дальних мобов.) ### Проверка - `make sql` — чисто. Поведение — рассуждение об эквивалентности гейта (выше). ### Что проверить тестировщикам - При `mob_ai: 0x20` на **пустой** карте (без игроков) мобы по-прежнему изредка бродят / телепортируются к спавну с тем же ~1с каденсом, что и раньше. - При `mob_ai: 0` (дефолт) поведение дальних мобов не изменилось. --- ## Опт-3 — грид присутствия игроков: пропуск заведомо пустых area-сканов ### Что было В `mob_ai_sub_hard` каждый обычный агрессивный/преследующий моб каждые 100 мс делал `map_foreachinrange(activesearch|changechase, view_range, BL_PC|BL_HOM)` — площадный скан вокруг себя в поиске игрока/гомункула. При `mob_ai&0x20` это выполняется для **всех** мобов карты, в т.ч. вдали от игроков, где скан гарантированно ничего не находит. ### Что стало Заведена точная по-блочная сетка присутствия `BL_PC|BL_HOM` (`src/map/mobgrid.c`, поле `map_data.block_pc_count`). Перед сканом для обычного моба (`!special_state.ai`) проверяется `map_pc_near(m,x,y,view_range)`: если ни одного PC/HOM нет в радиусе — скан пропускается (`tbl` остаётся NULL → тот же idle-путь, что и при пустом скане). Поведение идентично. Корректность (почему нет ложных «никого нет» → моб не перестанет агрить): - **Точный счётчик.** `block_pc_count` инкрементируется в `map_addblock_sub` и декрементируется в `map_delblock_sub` на **каждое** добавление/удаление PC/HOM (а не только головы списка, как у `block_count`). `map_moveblock` при смене блока идёт через del+add → перемещение игрока учитывается. Индекс блока — тот же `pos`/`b`, что у `block_count` (не может разойтись). - **Точное покрытие.** `bxs = ceil(xs/8)`, поэтому блок-диапазон `map_pc_near` **в точности** совпадает с диапазоном, который обошёл бы `map_foreachinrange` для того же `(x,y,range)` → консервативный надмножество, без ложных negative. - **Спец-AI мобы** (призывы, `special_state.ai`) ищут `BL_CHAR` (вкл. мобов) — для них грид не применяется, они всегда сканируют. - `changechase` использует `search_size ≤ view_range`, гейт по `view_range` покрывает и его. RAM: один `int` на блок карты (≈ `bxs*bys*4` байт, единицы–десятки КБ на карту). ### Тест `src/map/test_mobgrid.c` — standalone: ``` gcc -DMOBGRID_TEST -I src/map -o /tmp/test_mobgrid src/map/mobgrid.c src/map/test_mobgrid.c /tmp/test_mobgrid ``` Покрывает: нахождение PC в своём/соседнем блоке в пределах view_range, отсутствие за пределами, клэмп границ (нижний/верхний угол), точность inc/dec и защиту от ухода счётчика ниже 0. Прогон с `-fsanitize=address,undefined` — чисто. ### Проверка - Unit-тест — PASS, ASan/UBSan — чисто. - `make sql` — чисто, без новых предупреждений; boot-smoke — без крашей, без утечек (грид alloc/free сбалансирован). ### Что проверить тестировщикам (на кластере, `mob_ai: 0x20`) - **Агр работает на дистанции view_range.** Агрессивный моб замечает и атакует игрока ровно с того же расстояния, что и раньше (не «просыпается» позже). - Гомункулы провоцируют агр так же, как игроки. - Подходя к скоплению мобов из-за края экрана — агр срабатывает без задержки. - Профиль под нагрузкой: доля `map_foreachinrange`/`activesearch` в mob AI упала.