Сервер CS 1.6 без AMX Mod: плагины на Go
Я переписал свой сервер Counter-Strike 1.6 LegacySentry так, что в нём нет ни одного плагина AMX Mod X: вся игровая логика — около 50 модулей на Go внутри одной библиотеки. Сервер уже работает, и в него можно зайти прямо сейчас — ссылка в конце.
++++
Иллюстрация: пушка-сентри, раздатчик и лазерная мина — всё это на сервере написано на Go.С чего всё началось
У старого LegacySentry была классическая сборка: HLDS, Metamod, AMX Mod X и стопка плагинов на Pawn — CSDM, пушки, раздатчики, порталы, лазерные мины, VIP, статистика, голосование за карту. Всё это годами росло, и в какой-то момент добавить новую фичу стало страшнее, чем написать её с нуля.
Проблемы были знакомые всем, кто держал сервер на AMXX:
- плагины общаются через глобальные переменные, нативы и форварды, и никто не знает, кто кого сломает;
- Pawn — язык без нормальных структур, строк, пакетов и тестов;
- база данных, HTTP, Telegram — либо через сторонние модули на C, либо никак;
- один плагин упал — и непонятно, что вообще случилось.
Толчок: Claude Code
На самом деле всё началось с того, что мне захотелось разрабатывать сервер вместе с Claude Code: дорабатывать модули, добавлять новые фичи, искать баги. И быстро стало видно, что упирается он не в себя, а в платформу.
Не то чтобы Claude плохо писал на Pawn — писал вполне сносно. Но многие вещи, которые хотелось сделать, на AMXX делаются очень обходным путём: база данных, сеть, сложные структуры данных, свой интерфейс на клиенте. Каждая такая задача превращалась в поиск нужного модуля, форварда или оффсета.
Ещё важнее — проверка. Агенту нужно самому убедиться, что код работает: прогнать тесты, собрать, выложить на тестовый сервер, зайти туда ботом и посмотреть. С Pawn такой цикл не строится: тестов нет, а логика не отделяется от движка. На обычном языке с нормальными инструментами Claude Code раскрывается совсем иначе.
В итоге весь сервер написан с помощью ИИ-агентов: Claude Code и Codex. Ядро на Go, клиент, сайт, админка, лаборатория — всё это код агентов. Моя роль — решать, что делать, и проверять в игре, как это ощущается. Агенты работают параллельно и делят одни и те же стенды лаборатории.
Так появился LegacySentry 2: тот же функционал, но на своём фундаменте. Правило с первого дня — AMXX нет и не будет.
C, C++ или Go: как выбирали
Решение приняли за один вечер, 23 октября 2026 года, — и не на словах, а после двух замеров на тестовом сервере. Всё обсуждение шло прямо в Claude Code.
Варианты
- C или C++ внутри процесса. Родной язык движка, самый быстрый путь. Но любая ошибка с памятью роняет весь сервер, а строки, база и сеть даются тяжело.
- Go отдельным процессом и мост по сети. Безопасно, но каждое игровое событие едет через сокет — для стрельбы турели не годится.
- Go внутри процесса через свой мост на C++. Сначала казалось экзотикой. Перелом случился, когда я сформулировал аналогию: AMX Mod X — это тоже мост на C++ плюс плагины на Pawn. Значит, можно сделать то же, но с Go вместо Pawn.
Замеры
На тестовом сервере собрали пробный Go-плагин и сравнили его с Pawn и чистым C на одной карте. Потом сделали вторую пробу — кусок турели на Go.
- Пустой вызов движка из плагина, нс: Go — 110, Pawn — 12
- Вызов плагина из движка, нс: Go — 210–250, Pawn — 60
- TraceLine с чтением результата, нс: Go — 317, Pawn — 400, C — 199
- Расстояние между точками, нс: Go — 13, Pawn — 55
- Чтение HP игрока, нс: Go — 0,4 (из памяти), Pawn — 27 (
pev())
Итог вышел честный и не в пользу Go по всем статьям. Переход между движком и плагином у Pawn дешевле в 4–9 раз. Зато своя логика на Go быстрее, а поля сущностей можно читать прямо из памяти вообще без перехода — в 70 раз быстрее pev(). Для турели с 50 трассировками разница — 20 мкс против 16 мкс при кадре порядка миллисекунды. Скорость оказалась не решающей.
Заодно проверили надёжность: три вида ошибок (пустая map, выход за границу массива, битая память) перехватываются, сервер живёт дальше. Самая длинная пауза сборщика мусора — 2,9 мс.
Почему Go
Раз скорость не решает, выбирали по другому критерию: чтобы плагины было проще писать и чтобы они были стабильнее.
- Ошибка не роняет сервер. В C++ та же ошибка положила бы игру, в Go её ловит
recover. - Тесты без сервера. Логику можно проверять обычным
go test — для стабильности это важнее всего. - Удобно для ИИ. Самописные плагины старого сервера — около 13 тысяч строк на Pawn, одна только турель — два файла по 2,5 тысячи. Такие файлы без типов и без нормального LSP сжигают токены агента. В Go файлы по 100–300 строк, а gopls сразу показывает ошибки.
- Нормальный язык. Типы, структуры, строки, пакеты — всё, чего не хватало в Pawn.
В тот же вечер решили: выключаем все Pawn-плагины и сам AMXX, переписываем на Go. Через час появился новый репозиторий с первым коммитом.
Проба же показала и первые ловушки. Фоновые горутины на всех ядрах задерживали кадры на 30–80 мс. Отсюда правило: внутри процесса только реакция на события, а движок — только из главного потока. Остальные минусы — ниже, в отдельном разделе.
AMX Mod X — это, по сути, плагин Metamod со встроенной виртуальной машиной Pawn. Я сделал то же самое, но вместо Pawn взял Go. Получился один файл core_i386.so: тонкий слой на C++ принимает вызовы движка и игры, а вся логика живёт в Go.
++++
Наш талисман: гофер Go, который пришёл чинить Counter-Strike. Оригинальный гофер — Renée French.Под ним — не стоковый HLDS, а ReHLDS и мой форк ReGameDLL: у них есть цепочки хуков (hookchains) на реальные игровые события, и то, что на AMXX делалось через Ham Sandwich и оффсеты, тут делается честным API. А если нужного хука нет — дописываю его в форк сам.
Как это работает
Каждый вызов движка проходит один и тот же путь: хук в C++, экспортированная функция Go, реестр событий и дальше модули, которые на него подписаны.
- Движок (ReHLDS и ReGameDLL) вызывает хук в тонком слое на C++ внутри
core_i386.so. - Хук зовёт экспортированную Go-функцию через cgo.
- Реестр событий
event раздаёт событие модулям, которые на него подписаны. - Модуль решает, что сделать, и говорит это менеджерам: меню, тосты, HUD.
- Менеджер отправляет игроку сообщение — нашему клиенту своё, обычному CS 1.6 стандартное.
Обратно в движок Go ходит через функции core_* и regame_*, а модули видят только пакет engine и менеджеры.
Слой на C++
metamod.cpp — вход Metamod и хуки DLL (спавн, кадр, команды игрока). regame.cpp находит ReGameDLL, берёт его API и вешает hookchain'ы игры. Дальше C++ ничего не решает — он зовёт Go.
Ядро на Go
- Пакет
engine — единственная дверь к движку. Типы движка переведены в Go-структуры через cgo -godefs, поля сущностей читаются прямо из памяти: engine.Ent(p).V.Health. - Пакет
event — реестр событий. Ядро шлёт событие один раз, модули подписываются сами. - Общие сервисы: консольные команды, настройки в базе, аккаунты и роли, меню, тосты, чат, HUD.
Модуль — это папка
Парашют, сентри, порталы, голосование за карту — каждый в своей папке server/core/<имя>/: логика, связка с движком, модели, звуки, миграции базы. Всё подключение — из init(), а чтобы модуль попал в сборку, достаточно одной строки с пустым импортом.
++++
Кадр из игры: раздатчик, три лазерные мины и портал — три разных модуля, три папки в server/core.1234567
var killHP = settings.NewNumber("csdm.kill_hp", 10, "Сколько HP за убийство").Range(0, 255)
func init() {
event.OnSpawn("csdm", onSpawn) // игрок появился
console.Client("ls2menu", menuCommand) // команда клиента
palette.Add(...) // команда в палитре игрока
}
И самое важное правило: модуль не знает, какой у игрока клиент. Он говорит «покажи меню» или «покажи тост», а общий менеджер решает, как доставить: нашему клиенту — своими сообщениями с красивым интерфейсом, обычному CS 1.6 — стандартными меню и HUD. Поэтому играть можно и с обычного клиента.
База и остальные части
Аккаунты, роли, статистика, настройки и точки на картах лежат в PostgreSQL. Рядом живут сайт и админка — отдельные программы, у которых с сервером общая только база. Поменял настройку в админке — сервер подхватит её на следующей карте, без перезапуска.
Свой клиент
К серверу мы сделали и свой клиент — LegacySentry Next. В основе лежит Xash3D FWGS, открытый движок, совместимый с GoldSrc, и открытый клиент CS 1.6 к нему. Оба мы взяли к себе в репозиторий и правим как свой код. Пишет его тот же Claude Code.
Игра остаётся той же CS 1.6 — та же стрельба, движение и карты. Меняется всё вокруг:
- Современный интерфейс. Меню, тосты, баннер с подсказками, числа урона вокруг прицела, значки над игроками, личный HUD со званием, очками и карточкой VIP. Сервер присылает данные, а рисует их клиент.
- Палитра вместо чата. Одна строка ввода по центру экрана, как Spotlight: чат всем или своим, команды с поиском и описаниями. Набираешь «пуш» — и палитра предлагает поставить пушку. Список команд у каждого свой: его собирает сервер по ролям игрока.
- HD-графика. Текстуры карт и модели в высоком разрешении, а свет и анимации вещей клиент считает сам, без нагрузки на сервер.
- Аккаунт без регистрации. При первом заходе сервер выдаёт клиенту ключ, и дальше узнаёт игрока по нему: статистика, звание и VIP не теряются, даже если сменить ник.
- Самообновление. При запуске клиент сам докачивает изменившиеся файлы. Новая версия доходит до игроков сама, ничего скачивать заново не нужно.
- Везде. Windows, нативно Mac на Apple Silicon, Linux и даже Steam Deck. Steam не нужен.
++++
Палитра: набрал «пуш» — получил команду /sentry с описанием.++++
Меню игрока: постройки, оружие, карта и срок VIP — сервер описал данными, клиент нарисовал.Зачем играть именно с ним
Зайти на сервер можно и с обычного CS 1.6: все фичи работают, меню и подсказки придут стандартными. Но со своим клиентом играть заметно приятнее: видно урон, звание и состояние своих построек, команды не нужно запоминать, карты выглядят свежее. Установка — один архив с сайта, дальше он обновляется сам. А новые штуки интерфейса появляются сначала именно в нём.
И ещё одна важная для меня вещь: именно в этот клиент встроен тестовый интерфейс, через который Claude Code сам играет и проверяет свою работу. Об этом — дальше.
Своя админка
Вместо консоли RCON и сторонних панелей — своя веб-админка на Python (FastAPI) и React. Это отдельная программа: с игрой у неё общая база, а команды в игру идут через локальный мост. Вход — по игровому аккаунту с ролью администратора.
Что в ней есть:
- Сервер вживую. Карта, игроки и боты, обновление примерно раз в две секунды. Кикнуть, сменить карту, убрать или добавить ботов — одной кнопкой.
- Настройки игры. Все настройки, которые объявили Go-модули, с описанием, умолчанием и пометкой «Изменено». Новая настройка в коде сама появляется в админке.
- Игроки и роли. Поиск по нику, в том числе по прежним никам, история входов, выдача ролей со сроком.
- Чат. Вся переписка игроков с историей, и можно написать в игру прямо из браузера.
- Карты. Загрузка новой карты с превью, без FTP и перезапуска.
- LS TV. Записанные игры и лучшие моменты: выбрал момент — получил ролик, из них делаются шортс для YouTube.
- Посещения и финансы. Кто и сколько играл за период, выручка от VIP.
Метрики — спасибо Go
Нормальный сбор метрик появился именно благодаря тому, что вся логика живёт в Go. Ядро само замеряет каждый кадр сервера: сколько он длился, сколько из него занял наш код и сколько — уборка памяти. Раз в минуту в базу уходит строка: игроки и боты, пинг и потери пакетов, процессор и память. Там же сбои: пойманные паники, ошибки базы, игроки, отвалившиеся по таймауту.
На главной админки всё это на графиках за сутки, неделю, месяц или три месяца. Пожаловались на лаги — сразу видно, чей это пинг и тормозил ли сервер. В AMXX такого не получить: плагин не видит, сколько времени кадра съели другие плагины.
++++
Графики за сутки. В подсказке к кадрам сервера: самый долгий кадр 24,6 мс, наш код занимает 6% времени сервера, самая долгая уборка памяти Go — 0,4 мс.++++
LS TV: сервер сам записывает игры и размечает яркие моменты, а кнопка «Нарезать» отдаёт запись рендереру, и тот снимает ролик.Лаборатория: Claude Code проверяет сам
Ради этого всё и затевалось: агент пишет модуль, сам выкладывает его на тестовый сервер, сам заходит туда игроком и сам смотрит, что вышло. Я вижу уже проверенный результат.
Пул стендов
На отдельном компьютере с Ubuntu крутится пул слотов. Каждый слот — свой набор контейнеров: игровой сервер, PostgreSQL и тестовый клиент с видеокартой и виртуальным экраном. Агент берёт слот командой make lab-acquire, выкладывает туда свою ветку, проверяет и отдаёт обратно make lab-release.
- Несколько агентов работают параллельно и не мешают друг другу: у каждого свой сервер, своя база и свой клиент.
- Пул масштабируется сам: не хватает слотов — запускает новые, стенд простаивает — гасит его. Если свободных нет, агент ждёт в очереди.
- Веб-пульт показывает экраны всех стендов вживую: кто занял слот, какую задачу делает, какая ветка. Можно сидеть и смотреть, как агент бегает по карте, а можно нажать «Управлять» и поиграть самому прямо в браузере, со звуком.
++++
Веб-пульт лаборатории сейчас: в первом слоте другая сессия Claude Code делает новую модель пушки, третий свободен и ждёт следующего агента. В каждой карточке — живой экран тестового клиента и кнопка «Управлять».Клиент с пультом для агента
В движок нашего клиента встроен тестовый интерфейс (harness): клиент слушает TCP-порт и отвечает JSON'ом. На стороне агента — утилита на Go make bot. В сборках для игроков этого интерфейса нет. Через него Claude Code умеет три вещи.
Управлять. Нажать клавишу, набрать текст в чат, повернуть камеру, дойти до точки, держать «вперёд + прыжок», выстрелить, кликнуть настоящей мышью в пункт меню.
$ make bot C="cmd say /sentry" # поставить пушку$ make bot C="hold fwd jump for=2s" # бежать и прыгать две секунды$ make bot C="wait LsToast text=Следующ" # дождаться нужного тоста
Читать состояние мира. Не угадывать по картинке, а спросить напрямую. У клиента — где стоит игрок, с какой скоростью движется, какое меню открыто и какие в нём пункты, какие тосты на экране, какие звуки играют, куда легла пуля. У сервера — игроки и сущности как их видит сам сервер (ls2_state). Плюс журнал всех сообщений сервера, уже разобранных.
Смотреть. make shot делает скриншот, и Claude его рассматривает: не уехала ли вёрстка меню, виден ли луч лазера, как выглядит новая модель. Кстати, кадры из игры в этой статье сняты именно так.
Получается полный цикл без человека: написать код → прогнать go test → выложить в слот → зайти ботом → нажать, посмотреть, сверить состояние → поправить. А нужен обычный клиент CS 1.6 — есть и такой стенд, на Windows-компьютере.
И для ручной проверки
Стенд удобен не только агенту. Когда хочется самому потрогать новую фичу, я просто прошу Claude Code развернуть нужную ветку на свободный стенд. Дальше захожу туда сам — из браузера через пульт или обычным клиентом — и проверяю руками: ставлю пушку, смотрю меню, играю против ботов.
Игроки на production ничего не замечают. Не понравилось — говорю, что поправить, и смотрю снова на том же стенде. Только когда всё устраивает, ветка уходит в main и оттуда на боевой сервер.
Что я получил
Главное — сервер стал обычным проектом на Go, с которым работают как с любым другим сервисом.
- Тесты. Нормальное тестирование появилось только с Go-модулями: на Pawn его по сути не было. Логика модулей отделена от связки с движком тегом сборки, поэтому
go test ./... идёт прямо на маке, без игры. Правила наград, голосования, очки и звания проверяются за секунды. - Горутины для всего медленного. Запросы в базу идут через очередь в своей горутине, мост с Telegram читает и отправляет сообщения в фоне, LS TV пишет демки и лучшие моменты, прямо из игры работает HTTP-сервер со статистикой и состоянием сервера для сайта. Игровой кадр никого из них не ждёт: легла база или сеть — игра идёт дальше, а запросы дождутся своей очереди. В AMXX такое делалось только через сторонние модули с колбэками, а здесь это обычный Go.
- Порядок в коде. Пакеты не зависят по кругу, модули говорят друг с другом через функции и события, а не через глобальные переменные. Компилятор ловит ошибки до выкладки.
- Сервер не падает от ошибки в модуле. Каждый вход из движка обёрнут в защиту: паника или плохой указатель пишутся в лог с именем функции, игра идёт дальше.
- Скорость. Чтение поля сущности прямо из памяти стоит около 1 нс, а
pev() в Pawn — около 27 нс. Собственная логика на компилируемом языке тоже быстрее интерпретатора Pawn. - Всё нужное — из коробки. PostgreSQL через pgx, мост с группой в Telegram, статус для мониторинга на сайте — обычный код на Go, а не сторонние модули с форума.
- Настройки в базе, а не в cfg. Каждая настройка объявлена в коде с умолчанием, границами и описанием, и админка показывает её без отдельной работы.
- Независимость от клиента. Раз модули не знают про клиент, свой клиент с новым интерфейсом и обычный CS 1.6 живут на одном сервере и играют друг с другом.
- Нормальные инструменты. Профилировщик, линтеры, IDE, git — и даже статистика кадров прямо из игры: сколько времени каждый кадр занял именно наш код.
Чем за это платишь
Честно: такой путь дороже стартует и подходит не всем.
- Всё пишешь сам. На AMXX есть тысячи готовых плагинов, здесь — ни одного. Каждую фичу старого сервера пришлось перенести вручную, а часть старых плагинов — ещё и разобрать без исходников, из скомпилированных
.amxx. - Нет горячей замены. В AMXX плагин можно поменять сменой карты. Go-библиотеку выгрузить нельзя, любая правка — это перезапуск сервера.
- Один Go на процесс. Второй Go-плагин рядом не загрузится, поэтому всё собирается в один
.so. Делиться отдельными плагинами с другими серверами так не выйдет. - Переход между C и Go не бесплатный. Вызов из C в Go стоит около 250 нс, из Go в C — около 110 нс. Горячие хуки вроде
Think, Touch и AddToFullPack приходится фильтровать ещё в C++, иначе они съедят кадр. - Движок — только из главного потока. Горутины не могут трогать игру напрямую: ответ из базы возвращается через очередь, которую разбирает следующий кадр. Об этом надо помнить всегда.
- 32 бита и cgo. Сервер 32-битный, сборка идёт под Linux i386 с
gcc -m32, и дебажить падение на стыке C++ и Go заметно сложнее, чем в чистом Go. - Мало людей в теме. Все знают Pawn, почти никто не пишет плагины CS на Go. На форуме ответа не найдёшь — только в исходниках ReHLDS и ReGameDLL.
Итог: сервер работает — заходите
Было ли оно того стоить? Для меня — да. Старт вышел дороже, чем просто скачать готовые плагины, зато теперь новая фича — это новая папка с тестами, а не ещё один плагин, который неизвестно с чем поссорится.
LegacySentry 2 уже работает: CSDM, пушки-сентри, раздатчики, порталы, лазерные мины, парашют, усиления, звания и статистика — всё на Go. Зайти можно и с обычного CS 1.6, и с нашего клиента с новым интерфейсом.
++++
Пушка третьего уровня: стреляет сама, на высоком уровне добавляются ракеты.++++
Раздатчик четвёртого уровня лечит союзников синими лучами.++++
Лазерные мины: луч через проход, враг его не пройдёт.Попробуйте сами: legacysentry.ru — там адрес сервера, клиент и статистика игроков. Увидимся в игре.