Хуки геймода
В проекте два вида хуков: собственные события геймода и переопределения движковых. Устроены они по-разному.
Пара «разрешение и уведомление»
Почти каждое действие игрока обёрнуто двумя хуками. OnPlayerX спрашивает разрешение до действия и возвращает false для запрета либо true и, если нужно, дополнительное значение — цену, возврат, размер зарплаты. PlayerXed сообщает о том, что действие уже состоялось, и его возврат никого не интересует.
Разрешающий хук реализуется в sv_player.lua всегда одинаково: сначала спросить класс профессии, и только если тот промолчал — применить общее правило.
Сравнение идёт именно с nil, а не через if canBuy then. Профессия должна уметь вернуть false, а при обычной проверке на истинность запрет был бы неотличим от «не вмешиваюсь».
Каждый новый метод профессии объявляется пустой заглушкой в DEF_PLY (sh_jobs.lua). Без заглушки player_manager.RunClass его не найдёт и напишет ошибку в консоль.
Движковые хуки объявляются через GM
Стандартный хук движка внутри геймода реализуется методом геймода:
hook.Add внутри геймода не используется — он плодит записи в реестре, которые обходятся при каждом событии. Ему остаётся место в библиотеках из lua/autorun/ и во внешних аддонах, которые от геймода зависеть не должны.
У такого подхода есть цена: GM:Event существует в одном экземпляре на реалм. Файл, подключённый позже, молча перезапишет более ранний, без всякой ошибки. Поэтому перед тем, как писать function GM:X, стоит убедиться, что GM:X ещё никем не занят.
Хуки живут в корневых файлах
Реализации GM:* собраны в корне gamemode/: на сервере это sv_player.lua и sv_entities.lua, на клиенте — cl_init.lua. Модуль своих GM:* не объявляет, он отдаёт наружу функцию, а корневой хук её вызывает.
Заодно это снимает проблему с одним определением на реалм. В cl_init.lua хуки объявлены после всех include, так что корень перекрывает модуль, а не наоборот, а покадровая работа нескольких модулей складывается в один GM:Think.
Замещение базового геймода
Геймод наследуется от base (см. darkrp.txt), и DEFINE_BASECLASS в нём не используется. Реализуя GM:X, который уже есть в базовом геймоде, вы замещаете его целиком — вызвать родительскую версию неоткуда.
Поэтому перед переопределением движкового хука надо посмотреть, что делает базовый, и перенести нужное к себе. Показательный случай — GM:CalcView: базовая реализация обслуживает транспорт, JOB:CalcView и WEAPON:CalcView, и если просто её заменить, перестанут работать прицелы у оружия. В cl_init.lua вся цепочка выписана заново, а камера смерти добавлена сверху.
Свои события
Модулю, которому нужна собственная точка расширения, не надо вешать второй слушатель на движковый хук. Он объявляет своё событие:
На такие события внешние аддоны подписываются обычным hook.Add — им ограничения геймода не касаются.
