Осторожно: Эта статья — не просто очередная ретроспектива с воспоминаниями, а конкретный разбор сетевой архитектуры и хаков, благодаря которым стал возможен мультиплеер в GTA аж на 1.000 человек одновременно. Если вам интересен геймдев и то, как работают игры под капотом — статья обязательна к прочтению!
GTA: San Andreas запомнилась нам как легендарная игра с немыслимым, по меркам 2004 года, масштабом. В ней был и огромный бесшовный мир, состоящий из трех городов, и массивный автопарк аж из 213 транспортных средств, и длинная интересная история с кучей сайд-активностей, прохождение которых занимает как минимум несколько дней, и даже элементы RPG с базовой прокачкой персонажа.
И вот всё в ней было идеально, кроме одного — мультиплеера. Rockstar не успела доделать полноценный режим сетевой игры, оставив лишь подачку в виде локального кооперативного режима на PS2, в котором даже миссии проходить было нельзя. Однако группа моддеров под руководством Kalcor смогла воплотить то, что не удалось сделать Rockstar — реализовать мультиплеер в GTA SA аж на 1.000 человек одновременно. И о том, как им удалось это сделать, я расскажу в рамках сегодняшней статьи!
❯ Предыстория
Серия GTA получила статус культовой задолго до выхода San Andreas. Ещё в 2001 году, на PS2 вышла GTA III, повествовавшая нам об истории немого гангстера по имени Клод, который волею судьбы был вынужден сражаться за свое выживание на улицах грязного и холодного Либерти-Сити. В отличии от прошлых игр серии, GTA 3 перенесла геймплей в полноценное 3D, умудрившись реализовать на скромном железе PlayStation 2 огромный и (почти) бесшовный город, состоящий из трех островов.
После прохождения одиночной компании, многим игрокам хотелось большего: причем речь шла не о DLC с новыми миссиями, а о полноценном мультиплеере, где можно было бы устраивать анархию с другими игроками. И что интересно — мультиплеер изначально планировался Rockstar, но по каким-то причинам был вырезан из игры (скорее всего нехватка времени), о чём говорят оставленные строки с упоминанием сетевой игры.
// Ошметки из American.gxt[FEM_MP] MULTIPLAYER[FEF_LAN] LAN[FEF_INT] INTERNET[FEN_CON] Connection[FEN_GAM] Find game[FEN_TY] Game type[FEN_GNA] Game name:[FEN_NAM] Name:[FEN_NCI] NOT CONNECTED TO THE INTERNET[FEN_NET] Network[FEN_PLA] Number of players:[FEN_PLC] Player color[FEN_PLS] Player settings[FEN_UKH] Unknown host[FEN_UKM] Map not found[FEN_UKT] Game type not found[FET_MAP] SELECT MAP
Через год, в 2002 году, игра вышла на ПК и моментально привлекла внимание моддеров. Точную хронологию событий восстановить сложно, но скорее всего моддеры сначала написали софт для распаковки/запаковки текстур в форматах txd, затем импортеры и экспортеры 3D-моделей в формате dff для 3ds Max/Milkshape 3D, и параллельно с этим ковыряли скриптовый движок игры под названием SCM. SCM представлял из себя специализированную виртуальную машину, которая загружала огромную многопоточную программу под названием main.scm, которая в свою очередь контролировала чуть ли не половину геймплея игры: зоны банд, места спавна машин, пикапы оружия и здоровья, а также содержала логику миссий вместе с побочными заданиями.
Схожая система использовалась и в GTA2, причём DMA Design (бывшее название Rockstar North) в 1999 году опубликовала официальный компилятор вместе с документацией и примерами скриптов — что и послужило отправной точкой для реверса SCM (под «капотом» обе работали похожим образом, но у GTA2 номера опкодов другие):
Откуда там потоки?
Конечно многопоточность в SCM была условной, потоки в скриптах не могли выполняться параллельно на разных ядрах процессора и не контроллировались планировщиком задач в ОС. Вместо этого, у движка игры был собственный планировщик, который каждый кадр брал по одной инструкции каждого потока и выполнял только её. Такой подход позволяет не превращать игровой цикл в мешанину из коллбэков, корутин и await’ов и сохранить синхронность и последовательноть скриптовой части игры.
Фича настолько удобная, что Oracle реализовала эту концепцию за счет виртуальных потоков в Java 21 🙂
Первой попыткой реализовать мультиплеер в GTA 3 стал GTA3: AM aka MTA 0.1, выпущенный через год после релиза игры на ПК — 9 февраля 2003 года. Первая версия мода была написана на VB6 (да, да, звучит дико) немецким подростком и являлась результатом реверс-инжиниринга кода игры, в котором на тот момент разобрали только структуру класса автомобиля и игрока. Из-за этого синхронизировалось только две машины, а для того чтобы увидеть второго игрока, нужно было сначала сесть в машину в которой будет ехать второй игрок, затем выйти из нее и сесть в «свою» — и только после этого игроки смогут увидеть друг друга, причем вторая машина будет без водителя.
В MTA 0.2 добавили чат и пофиксили краши, а в MTA 0.3 реализовали полноценную синхронизацию других игроков вместе с первыми игровым режимами — Deathmatch и Stunt. Именно в 0.3 впервые использовалась своя собственная реализация main.scm — mta.scm, за счет которой, скорее всего, и реализовали синхронизацию игроков: скриптовый движок позволяет создавать пешеходов и машины, а также управлять их позицией, поворотом, ускорением и даже может заставить их стрелять в определенном направлении — и этого было достаточно для базовой синхронизации игроков. Весной 2003 года вышла GTA: Vice City на ПК, которая использовала всё тот же движок, и за короткое время разработчики перенесли наработки в новую игру, а к команде присоединился австралиец Kalcor, который позже станет известен как разработчик SAMP:
Однако разработчики отнюдь не работали «вслепую». Как рассказывал Kalcor, примерно в 2004 году, у команды MTA появились связи с программистами из Rockstar, которые неофициально делились заголовочными файлами с описанием структур и функций игры. Выяснилось что контекст скриптового движка можно легко «угнать», если подсунуть ему свою структуру CRunningScript с указателем на произвольный буфер с опкодами, и заставить игру делать всё что пожелаешь. Именно эта находка позже легла в основу CLEO (ближе к версии 3.0), который позволил подключать к игре кастомные скриптовые моды и даже расширять её функционал за счет добавления новых опкодов.
Изначально сервер в MTA занимался лишь пересылкой состояния игроков, в то время как игровые режимы реализовывались на клиентской стороне. Kalcor’а это не устраивало, но команда стояла на своём — так что в 2005 году он покинул проект, чтобы начать свой — VCMP с RakNet в качестве сетевого транспорта и скриптингом на стороне сервера. В качестве скриптового движка он выбрал AMX — маленький, но очень шустрый C-подобный язык, пригодный для написания небольших игровых режимов (и совершенно непригодный для написания всяких RP — страшно представить, что творится в коде условного Radmir/Arizona RP), который до этого использовался для написания серверных модов в CS 1.6 (помните AMX Mod X? :)) и хорошо зарекомендовал себя.
Скрытый текст
Команда VCMP чуть позже выбросила AMX и перешла на Squirrel — куда более пригодный язык для написания больших режимов, в то время как SAMP так и остался на AMX. Скорее всего Kalcor просто устал в 2010-х поддерживать свой мод и у него не было времени или желания добавлять что-то ещё.
В том же 2005 году, GTA: San Andreas наконец-то вышла на ПК и Kalcor принялся портировать наработки VCMP для неё, благо движок оставался всё тем же. Самая первая версия SAMP поддерживала только гонки по чекпоинтам, но в мае 2006 года вышла полноценная публичная бета, заработал сайт sa-mp.com и с этого всё началось…
Оригинальная версия могла обрабатывать до 100 игроков на одном сервере (что уже немало), но Kalcor продолжал дорабатывать мультиплеер, и в версии 0.2 лимиты подняли до 200, в 0.3 — до 500, а в 0.3c — аж до 1000 игроков одновременно! Это уже вполне уровень MMO. Kalcor достиг этого за счет динамического стриминга объектов на сервере: если игрок или автомобиль находится вне зоны стриминга (которая определялась сферой радиусом в 150м вокруг игрока), то сервер банально переставал слать информацию об обновлениях объекта и клиент его удалял — но если 1000 игроков собирались в одном месте… FPS на большинстве компьютеров тех лет падал в однозначные значения.
В последующие годы SAMP активно развивался — добавляли новые функции, объекты и скины, пока в 2015 не вышел последний крупный апдейт — 0.3.7, который добавлял статических NPC. После этого, активная поддержка модификации закончилась… а вот серверов — нет. С 2010 по ~2020 год, SAMP проживал свои лучшие дни: серверы уже не ограничивались простыми DM/Stunt/Cops And Robbers режимами на пару сотен человек, администраторы серверов активно вкладывались в RP и RPG режимы, которые превращали GTA в симулятор реальной жизни с отыгрышем ролей, работами со своим начальством из реальных игроков, собственной сложной экономикой и имуществом, которое стоило реальные деньги (условные бизнесы на Radmir могут продавать за 10-15 тысяч реальных рублей). Доход этих проектов исчисляется миллионами рублей, а их авторы смогли себе позволить практически полный порт GTA SA на Unity под мобильные устройства…
❯ Изучаем архитектуру
Но в чем секрет SAMP и как он работает под капотом? В студенческие годы я и сам писал свой мультиплеер для Vice City, который работал как External-чит, написанный на C# (звучит дико, но оно работало на базовом уровне), изучая исходный код SAMP 0.2. И в нём есть много фишек, о которых будет интересно узнать как молодым инди-разработчикам, так и реверс-инженерам и простым игрокам.
Детали реализации моего мультиплеера
Моя реализация мультиплеера работала за счет перезаписи процесса игры функциями ReadProcessMemory и WriteProcessMemory, а также инъекций шеллкода. Вместе с клиентом шел ассемблер FASM, который на лету компилировал мелкие функции по типу «угнать скриптовый контекст, выполнить n опкодов и вернуть результат», выделял в памяти игры небольшой пул для данных с помощью системного аллокатора VirtualAlloc, и затем выполнял код с помощью функции WinAPI CreateRemoteThread.
За счет этой же концепции работают инжекторы dll-библиотек, которыми пользуются всяческие кряки для софта, моды и читы для онлайн-игр. В целом, никто не мешает инжектнуть и Managed библиотеку в нативную игру при условии использования .NET Framework: тогда библиотеку можно загрузить через COM по CLSID с помощью шеллкода, либо внедрить рантайм .NET в игру с помощью ICLRHostControl, либо инжектнуть обычную C++/CLR библиотеку в качестве нативной DLL, которая создаст контекст .NET. Для обычных .NET без Framework остаются способы с инжектом hostfxr и через COM.
Кроме того, маршаллер в .NET позволяет без проблем вызывать нативные функции через GetDelegateForFunctionPointer, в том числе и виртуальные, если первым аргументом идёт указатель на экземпляр. Так что возможности хукинга в .NET очень обширные 🙂
Начнем с вами с клиентской части, которая инжектится в игру как обычная dll-библиотека в качестве asi-плагина. После загрузки, ОС вызывает функцию DllMain, в которой создается поток, ожидающий полноценного запуска игры:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){if(DLL_PROCESS_ATTACH==fdwReason){OutputDebugString("SA-MP Process Attached\n");hInstance = hinstDLL;InitSettings();//InitKeybinds(); // unfinishedif(tSettings.bDebug || tSettings.bPlayOnline) {//SetUnhandledExceptionFilter(exc_handler);//dwGameLoop = (DWORD)TheGameLoop;dwGraphicsLoop = (DWORD)TheGraphicsLoop;if(tSettings.bPlayOnline) {}_beginthread(LaunchMonitor,0,NULL);OutputDebugString("SA:MP Inited\n");}return TRUE;}... void LaunchMonitor(PVOID v){pGame = new CGame();pGame->InitGame();while(1) {if(*(PDWORD)ADDR_ENTRY == 7) {if(tSettings.bPlayOnline) {pGame->StartGame();break;} else if(tSettings.bDebug) {InitScripting();ApplyDebugLevelPatches();break;}}else {Sleep(5);}}ExitThread(0);}
После того как игра инициализировалась до этапа показа стартовых роликов, клиент хукает состояние меню так, чтобы игра сразу перешла к загрузке мира, а также «угоняет» главный цикл игры и контекст D3D9 для отрисовки оверлея с чатом и HUD’а над головами других игроков. Угон происходит за счет патчинга функции Render2DStuff, в начало которой записывается инструкция jmp с указателем на функцию, которая будет вызываться перед выполнением оригинального кода.
void CGame::StartGame(){//ApplyInGamePatches();// Install all hooksGameInstallHooks();// Setup scriptingInitScripting();*(PDWORD)ADDR_ENTRY = 8;*(PBYTE)ADDR_GAME_STARTED = 1;*(PBYTE)ADDR_MENU = 0;*(PBYTE)ADDR_STARTGAME = 0; /* Прим. автора: обратите внимание на то, как просто устроена стейтмашина в игре :) */}...InstallCallHook(0x609A4E,(DWORD)PlayerWalk_Hook);InstallCallHook(0x4579C6,(DWORD)PickUpPickup_Hook, 0xE9);InstallCallHook(0x73C252,(DWORD)CWeapon_FireCamera_Hook, 0xE9);InstallCallHook(0x6B2028,(DWORD)AllVehicles_ProcessControl_TankTurret_Hook);InstallCallHook(0x6B1F5E,(DWORD)AllVehicles_ProcessControl_WaterTurret_Hook);...UnFuck(0x53EB13,4);*(int *)0x53EB13 = dwGraphicsLoop - 0x53EB12 - 5; // relative addrInstallHook(0x58C246,(DWORD)GameProcessHook,0x53BED1,GameProcess_HookJmpCode,sizeof(GameProcess_HookJmpCode));
Хук главного цикла происходит аккурат в тот момент, когда игра закончила отрисовку всех объектов на экране и перешла к отрисовке интерфейса. Именно в этот момент клиент выполняет синхронизацию игроков, спавнит объекты и занимается прочими полезными делами, при этом каждая серверная сущность (игрок, автомобиль, объект, пикап и меню) имеет свой отдельный фиксированный пул.
void CNetGame::Process(){//UpdateNetwork();if (m_byteHoldTime){pGame->SetWorldTime(m_byteWorldTime, m_byteWorldMinute);}pGame->SetWorldWeather(m_byteWeather);// KEEP THE FOLLOWING ANIMS LOADED DURING THE NETGAMEif(!pGame->IsAnimationLoaded("PARACHUTE")) pGame->RequestAnimation("PARACHUTE");if(GetGameState() == GAMESTATE_CONNECTED) {DWORD dwStartTick = GetTickCount();if(m_pPlayerPool) m_pPlayerPool->Process();if(m_pActorPool) m_pActorPool->Process();...}if( GetGameState() == GAMESTATE_WAIT_CONNECT && (GetTickCount() - m_dwLastConnectAttempt) > 3000) {if(pChatWindow) pChatWindow->AddDebugMessage("Connecting to %s:%d...",m_szHostOrIp,m_iPort);m_pRakClient->Connect(m_szHostOrIp,m_iPort,0,0,5);m_dwLastConnectAttempt = GetTickCount();SetGameState(GAMESTATE_CONNECTING);}}
В каждом пуле есть несколько сотен слотов для сущностей, причем память для них выделена заранее. При получении сообщения от сервера вида WorldPlayerAdd/WorldVehicleAdd, клиент вызывает функции игры и физически создает объект в мире игры, а сами пакеты присылаются сервером в момент входа в зону стриминга:
void WorldPlayerAdd(RPCParameters *rpcParams){RakNet::BitStream bsData((unsigned char*)Data,(iBitLength/8)+1,false);CRemotePlayer *pRemotePlayer;CPlayerPool *pPlayerPool = pNetGame->GetPlayerPool(); ...bsData.Read(playerId);bsData.Read(byteTeam);bsData.Read(iSkin);bsData.Read(vecPos.X);bsData.Read(vecPos.Y);bsData.Read(vecPos.Z);bsData.Read(fRotation);bsData.Read(dwColor);bsData.Read(byteFightingStyle);bsData.Read(bVisible); ...if(pPlayerPool) {pRemotePlayer = pPlayerPool->GetAt(playerId);if(pRemotePlayer) pRemotePlayer->Spawn(byteTeam,iSkin,&vecPos,fRotation,dwColor,byteFightingStyle,bVisible);}}
Создание и манипуляция объектами происходит за счет угона контекста скриптового движка SCM, о котором мы говорили ранее. Клиент заставляет игру создать еще одного игрока/машину/пикап (эта же функция используется для кооператива на PS2) и перехватывает над ним управление. Тут есть интересный момент — по умолчанию в GTA нет нормальной системы динамической подгрузки ресурсов «по необходимости», все модели и анимации скрипты подгружают вручную с помощью опкодов 0x247 и 0x4ED, а если этого не сделать — то игра крашнется.
CPlayerPed::CPlayerPed(BYTE bytePlayerNumber, int iSkin, float fX, float fY,float fZ,float fRotation){// CREATE PLAYERScriptCommand(&create_player, &iPlayerNum, fX, fY, fZ, &dwPlayerActorID);ScriptCommand(&create_actor_from_player,&iPlayerNum,&dwPlayerActorID);m_dwGTAId = dwPlayerActorID;m_pPed = GamePool_Ped_GetAt(m_dwGTAId);m_pEntity = (ENTITY_TYPE *)m_pPed;m_bytePlayerNumber = bytePlayerNumber;SetPlayerPedPtrRecord(m_bytePlayerNumber,(DWORD)m_pPed);ScriptCommand(&set_actor_weapon_droppable,m_dwGTAId,1);ScriptCommand(&set_actor_immunities,m_dwGTAId,0,0,1,0,0);ScriptCommand(&set_actor_can_be_decapitated,m_dwGTAId,0);SetMoney(pNetGame->m_iDeathDropMoney); ...SetModelIndex(iSkin);SetTargetRotation(fRotation);GameResetPlayerKeys(m_bytePlayerNumber);}
В свою очередь, контекст SCM угоняется путем создания экземпляра объекта CRunningScript с заполнением указателя на текущую инструкцию (dwIP) и стека (если это нужно). После того как скриптовый движок обработал команду, хук достает результат выполнения (например ID созданной машины или персонажа) из стека и сохраняет его как хэндл для манипуляции объектов в будущем:
int __declspec(naked) ExecuteScriptBuf(){__asm{mov bScmLocalDebug, 1;mov eax, OFFSET ScriptBufmov ecx, gst// В ecx сохраняется указатель на объект CRunningScriptmov [ecx+0x14], eax// В eax указатель на IP (буфер с опкодами)call ProcessOneCommand// После чего вызывается функция ProcessOneCommand // которая выполняет ровно один опкод в буфере // (эта же функция используеся игрой для реализации многопоточности)mov ecx, gstmov eax, [ecx+0xC5]// Далее указатлеь на флаг, означающий вернул ли опкод значение запимыватся в eaxmov bScmLocalDebug, 0;ret// return.}}
Интереснее всего выполнена синхронизация управления персонажами. Помните я говорил о том, что в PS2-версии GTA SA был локальный кооператив на двух игроков? Так вот: у каждого объекта игрока есть своя структура с описанием того, какие кнопки управления сейчас зажаты. Поэтому ходьба, стрельба и прицеливание реализованы как раз за счет прямой синхронизации нажатых кнопок: ваш клиент помимо координат и ускорения отсылает ещё и состояние кнопок, в то время как другие игроки у вас «честно» бегают на клиенте, но при сильном рассинхроне позиции их откидывает на ту позицию, которую прислал сервер помимо нажатых кнопок. Из-за этого бег в SAMP’е был таким «рваным»:
А почему нельзя синхронизировать стейт отдельно?
Резонный вопрос: зачем симулировать кнопки, если можно вручную задавать текущую анимацию игрока и честно синхронизировать позицию и ускорение с поправкой на компенсацию лагов?
Тут проблема в том, что во первых такая анимация бега будет рассинхронизирована с движением персонажа (GTA активно использует Root motion) без информации о текущем проигрываемом кадре, а во вторых на эту систему подвязан механизм регистрации стрельбы — о котором я сейчас расскажу.
// LEFT/RIGHT if(pPlayerKeys->wKeys1[0] && lrAnalog) {pPlayerKeys->wKeys1[0] = lrAnalog;pPlayerKeys->wKeys2[0] = lrAnalog; } else if(lrAnalog && !pPlayerKeys->wKeys1[0]) {pPlayerKeys->wKeys1[0] = lrAnalog;pPlayerKeys->wKeys2[0] = 0; } else if(!lrAnalog) {pPlayerKeys->wKeys1[0] = 0;pPlayerKeys->wKeys2[0] = 0; }
Не менее интересно сделана и регистрация попаданий. Те читатели, которые застали ранние версии SAMP, могли вспомнить такое явление, как «стрельба по пингу»: если ты просто стреляешь по модельке бегущего персонажа — ты гарантированно не попадаешь из-за лага на сервере. Но если стрелять с небольшим упреждением (~20-30 пикселей от персонажа в сторону вектора его движения), то попадание засчитывалось. Происходило это как раз вследствие механизма синхронизации кнопок, который я описал в прошлом абзаце: клиент синхронизировал не сам факт попадания на нашем клиенте (нужную функцию игры для хукинга на тот момент просто не нашли), а лишь наш угол прицеливания и зажатую кнопку стрельбы. Таким образом, обработка попаданий ложилась полностью на того игрока, в которого мы стреляем — если мы попали в него на его клиенте, у него отнималось здоровье, которое отсылалось на сервер и засчитывалось как попадание. Но поскольку все пакеты доходят с заметной задержкой, за условные 120мс уже пройдет 7.5 кадров игры и игрок «у себя» давно убежит за пределы экрана, а мы у него будем стрелять «в молоко» 🙂
В 2014 году Kalcor всё таки нашёл нужную функцию для определения попадания на клиенте (предположительно хукнув условный CPed::InflictDamage) и смог реализовать нормальную обработку, добавив коллбэк OnPlayerTakeDamage и назвав её компенсацией лагов. С этого момента стрельба перестала быть ватной.
Как должно быть правильно?
В SAMP используется полуавторитарная модель мультиплеера: сервер отвечает за основные правила игры и игровой режим, но полностью доверяет игрокам в вопросах текущей позиции, ускорения и здоровья (и именно поэтому в SAMP’е так легко летать над картой, использовать спидхаки и прочие приколы).
В такой модели самым правильным решением является обработка попадания на клиенте того игрока, который стреляет и последующая отсылка пакета InflictDamage на сервер, что в свою очередь убирает все минусы «ватной» стрельбы, но порождает возможность реализации деструктивных читов по типу «зафлудить сервер пакетом InflictDamage и застрелить всех с другого конца карты» (что тоже можно без проблем детектить).
В авторитарных моделях (Quake, Half-Life, CS2, CoD и других играх) такие приколы вообще невозможны, поскольку клиент отсылает только кнопки и угол прицеливания (и ничего более, даже позицию не отсылает), в то время как весь геймплей обрабатывает сервер и отсылает на клиент лишь обновленное состояние мира, которое сглаживается с помощью компенсации лагов. С этим решением нет возможности читерить деструктивно (то есть спавнить объекты, телепортировать игроков в клетки и прочие прелести P2P и полуавторитарных моделей), но игра сильно зависит от тикрейта на сервере и пинга, играть нормально в CS с пингом 100+ невозможно, в отличии от игр с полуавторитарной моделью.
RakNet::BitStream bsUnoccupiedSync;UnSync.VehicleID = VehicleID;UnSync.vecMoveSpeed.X = vecMoveSpeed.X;UnSync.vecMoveSpeed.Y = vecMoveSpeed.Y;UnSync.vecMoveSpeed.Z = vecMoveSpeed.Z;UnSync.vecTurnSpeed.X = vecTurnSpeed.X;UnSync.vecTurnSpeed.Y = vecTurnSpeed.Y;UnSync.vecTurnSpeed.Z = vecTurnSpeed.Z;UnSync.vecPos.X = matVehicle.pos.X;UnSync.vecPos.Y = matVehicle.pos.Y;UnSync.vecPos.Z = matVehicle.pos.Z;UnSync.fHealth = pVehicle->GetHealth();bsUnoccupiedSync.Write((BYTE)ID_UNOCCUPIED_SYNC);bsUnoccupiedSync.Write((PCHAR)&UnSync,sizeof(UNOCCUPIED_SYNC_DATA));pNetGame->GetRakClient()->Send(&bsUnoccupiedSync,HIGH_PRIORITY,UNRELIABLE_SEQUENCED,0);CVehiclePool *pVehiclePool = pNetGame->GetVehiclePool();if (pVehiclePool) { pVehiclePool->SetLastUndrivenID(VehicleID, pPlayerPool->GetLocalPlayerID()); // us} pVehicle->m_bUnoccupiedSync = true;m_dwLastUndrivenUpdate = dwThisTick;
На сервере тоже есть на что посмотреть. В своей основе, сервер является ретранслятором сообщений от игроков: он принимает пакеты синхронизации, обновляет состояние игроков на серверной части и рассылает их другим игрокам. Тут всё по классике:
if(m_byteUpdateFromNetwork != UPDATE_TYPE_NONE) {BroadcastSyncData();m_byteUpdateFromNetwork = UPDATE_TYPE_NONE;}if (m_byteTime) // If this player has private time{if (m_fGameTime - fLastSync >= 300.0f) // Sync to server every 5 minutes (no chance of lagging in there really){if (m_fGameTime >= 1440.0f) m_fGameTime -= 1440.0f; // Limit to 24 hoursfLastSync = m_fGameTime; // Only do it here as it's not going to be far out and it means syncs are correctint iTime;iTime = (int)m_fGameTime;RakNet::BitStream bsPlayerTime;bsPlayerTime.Write((BYTE)(iTime / 60));bsPlayerTime.Write((BYTE)(iTime % 60));pNetGame->GetRakServer()->RPC(&RPC_SetTimeEx,&bsPlayerTime,HIGH_PRIORITY,RELIABLE,0,pNetGame->GetRakServer()->GetPlayerIDFromIndex(m_bytePlayerID),false, false, UNASSIGNED_NETWORK_ID, NULL);}}
Однако для управления поведением сервера предусмотрены режимы, написанные на скриптовом языке Pawno (тот самый AMX). На каждое событие сервер вызывает коллбэк, скрипт в свою очередь выбирает как на него реагировать: если персонаж сел в машину, в которую ему садится нельзя (допустим это ТС фракции, к которой он не принадлежит), то скрипт может его оттуда принудительно выкинуть.
Помимо игровых режимов, сервер также запускал так называемые фильтр-скрипты, которые получали всё те же коллбэки, но использовались как своеобразная система плагинов. На фильтрскриптах реализовывали античиты, логирование, а также маппинг карты: интереснее всего то, что кастомные объекты на карте создавались прямо из скриптов (вместо отдельного файла с описанием сцены), что в некоторых игровых режимах раздувало код до десятков тысяч строк в одном файле 🙂
int CGameMode::OnPlayerConnect(cell playerid){CHECK_INIT();int idx;cell ret = 0;if (!amx_FindPublic(&m_amx, "OnPlayerConnect", &idx)){amx_Push(&m_amx, playerid);amx_Exec(&m_amx, &ret, idx);}return (int)ret;}
У сервера были серьезные проблемы с оптимизацией. Например каждый вызов коллбэка, сервер искал функцию с её именем, что в свою очередь требовало итерацию по таблице символов вместе с strcmp. Именно поэтому сколь либо крупные паблики требовали серьезных серверных мощностей, несмотря на простоту серверной модели. А ещё, как и у любого уважающего себя сервера, у него был RCON — специальная консоль, с помощью которой сервером можно было управлять удаленно.
void RconCommand(RPCParameters *rpcParams){int iBitLength = rpcParams->numberOfBitsOfData;PlayerID sender = rpcParams->sender;BYTE byteLength;char szCommand[256];RakNet::BitStream bsData(rpcParams->input, (iBitLength/8)+1, false);bsData.Read(byteLength);bsData.Read(szCommand, byteLength);szCommand[byteLength] = '\0';logprintf("[RCON] Command: %s", szCommand);pNetGame->SendClientMessageToAll(0x2587CEAA, "* Admin: %s", szCommand);}
В целом, сервер был простым и шустрым в свой основе, но застой с AMX несколько тормозил прогресс игровых режимов, а из-за отсутствия клиентских модификаций все серверы выглядели «одинаковыми» (хотя тут ещё влияет преемственность RP-проектов, многие из которых берут корни от SAMP RP, и других режимов типа DM-Stunt).
❯ Заключение
Вот так работал легендарный мод для GTA «под капотом». И хотя его сетевая модель выглядит достаточно простой, тысячи человеко-часов, потраченные Kalcor’ом на реверс-инжиниринг и отладку кода вызывают неподдельный гиковсий респект! Но история мультиплеера в GTA на этом не закончилась, и для GTA V вышло как минимум 3 мультиплеера — alt:V, RageMP и FiveM. И что самое интересное — Rockstar Games купила команду FiveM в 2023 году, дабы превратить сторонний мод в полуофициальный мультиплеер, закрыв альтернативные проекты 🙁
А вы играли когда-нибудь в SAMP? Понравилась ли вам статья? В ближайшее время я хочу сделать полный разбор архитектуры GTA III/VC/SA со всеми интересными фишками и оптимизациями «под капотом», если вам интересен такой формат — дайте знать, обязательно сделаю 🙂
В качестве тестовых машин, мне было бы интересно пощупать что‑то ещё из семейства Nvidia ION/Radeon Xpress Graphics/VIA Chrome9 HC/Trident Blade. Если у кого‑то есть всякие интересные тонкие клиенты или ноутбуки на прикольном железе, или что‑то AMD’шное времен 939/462 — пишите в ЛС, обязательно сделаю контент!
Ну а я надеюсь, что вам было интересно. Подписывайтесь на блог, чтобы не пропускать новые статьи каждую неделю! А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram‑канал «Клуб фанатов балдежа„, куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты. А ролики (не всегда дублирующие статьи) можно найти на моём YouTube канале.“»
А если вы хотите что‑нибудь подарить из железа и увидеть о нём статью — пишите мне в Telegram. Меня очень интересуют самые разные гаджеты: начиная от игровых консолей и любых связанных с геймингом устройств, телефонов, смартфонов, КПК, заканчивая ретро‑компьютерами и ноутбуками. Кто знает, может героем следующей подобной статьи окажется ноутбук из 90-х?:‑)
После обзоров устройства не продаются, а остаются в моей коллекции. Когда‑нибудь я хочу сделать музей, где к каждому устройству можно будет приложить QR и почитать мою статью. Кто знает, вдруг на следующей неделе я также подробно расскажу про девайс из вашей юности?:)
Кстати, у меня есть GameBoy Advance SP, под который я очень хочу написать игру. Однако мой экземпляр был залит водой и кофе. Может у кого‑то есть донор с дохлой платой, откуда я смог бы взять контроллер питания? У меня AGS-101.
Может быть интересно:
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩
ссылка на оригинал статьи https://habr.com/ru/articles/1101956/