ESPN DeportesCanelo vs. Mbilli: Sigue en vivo la pelea, resultados y másThe Jerusalem PostElderly Haifa falafel stand owner beaten on way to work, two suspected attackers arrestedESPNWho is on upset alert in Week 9? Connelly breaks down 31 key matchupsPunchVIDEO: Police raid criminal hideouts in AnambraInquirerMayor Zamora inspects San Juan City Cemetery ahead of UndasZDF heuteEntdecken Sie das ZDF-Nachrichtenstudio7sur7“Aucune nécessité de rencontrer les Américains”: l’Iran ferme la porte aux négociations avec WashingtonCollider'Pirates of the Caribbean' Meets 'Black Sails' in Prime Video's New 6-Part SeriesХабр[Перевод] Маленький марсианский ровер, который остался домаSCMP ChinaQuad diplomats finalise first leaders’ summit in two years before Trump-Xi meetingBBC عربيجبارين في بلا قيود: منعنا نتنياهو من الأغلبية التي أرادها.Business AMVerenigde Staten testen inzet van F-16-gevechtsvliegtuigen vanaf afgelegen eiland bij Alaska door toenemende dreiging uit Rusland en China
The Daily Newsstand · Free, Always
Saturday, October 31, 2026

Как набор грязных хаков добавил мультиплеер в легендарную игру: разбираем архитектуру SAMP

Translate

Осторожно: Эта статья — не просто очередная ретроспектива с воспоминаниями, а конкретный разбор сетевой архитектуры и хаков, благодаря которым стал возможен мультиплеер в 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 огромный и (почти) бесшовный город, состоящий из трех островов.

Официальный скриншот GTA III для PS2

Официальный скриншот GTA III для PS2

После прохождения одиночной компании, многим игрокам хотелось большего: причем речь шла не о 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 для 3 ds Max/Milkshape 3D, и параллельно с этим ковыряли скриптовый движок игры под названием SCM. SCM представлял из себя специализированную виртуальную машину, которая загружала огромную многопоточную программу под названием main.scm, которая в свою очередь контролировала чуть ли не половину геймплея игры: зоны банд, места спавна машин, пикапы оружия и здоровья, а также содержала логику миссий вместе с побочными заданиями.
Схожая система использовалась и в GTA2, причём DMA Design (бывшее название Rockstar North) в 1999 году опубликовала официальный компилятор вместе с документацией и примерами скриптов — что и послужило отправной точкой для реверса SCM (под «капотом» обе работали похожим образом, но у GTA2 номера опкодов другие):

Откуда там потоки?

Конечно многопоточность в SCM была условной, потоки в скриптах не могли выполняться параллельно на разных ядрах процессора и не контроллировались планировщиком задач в ОС. Вместо этого, у движка игры был собственный планировщик, который каждый кадр брал по одной инструкции каждого потока и выполнял только её. Такой подход позволяет не превращать игровой цикл в мешанину из коллбэков, корутин и await'ов и сохранить синхронность и последовательноть скриптовой части игры.

Фича настолько удобная, что Oracle реализовала эту концепцию за счет виртуальных потоков в Java 21:)

Официальный скриншот GTA III для PC

Официальный скриншот GTA III для PC

Первой попыткой реализовать мультиплеер в GTA 3 стал GTA3: AM aka MTA 0.1, выпущенный через год после релиза игры на ПК — 9 февраля 2003 года. Первая версия мода была написана на VB6 (да, да, звучит дико) немецким подростком и являлась результатом реверс‑инжиниринга кода игры, в котором на тот момент разобрали только структуру класса автомобиля и игрока. Из‑за этого синхронизировалось только две машины, а для того чтобы увидеть второго игрока, нужно было сначала сесть в машину в которой будет ехать второй игрок, затем выйти из нее и сесть в «свою» — и только после этого игроки смогут увидеть друг друга, причем вторая машина будет без водителя.

Код синхронизации автомобилей в MTA 0.1. sub_40D930 - это WriteProcessMemory :)

Код синхронизации автомобилей в MTA 0.1. sub_40D930 — это WriteProcessMemory:)

В MTA 0.2 добавили чат и пофиксили краши, а в MTA 0.3 реализовали полноценную синхронизацию других игроков вместе с первыми игровым режимами — Deathmatch и Stunt. Именно в 0.3 впервые использовалась своя собственная реализация main.scm — mta.scm, за счет которой, скорее всего, и реализовали синхронизацию игроков: скриптовый движок позволяет создавать пешеходов и машины, а также управлять их позицией, поворотом, ускорением и даже может заставить их стрелять в определенном направлении — и этого было достаточно для базовой синхронизации игроков. Весной 2003 года вышла GTA: Vice City на ПК, которая использовала всё тот же движок, и за короткое время разработчики перенесли наработки в новую игру, а к команде присоединился австралиец Kalcor, который позже станет известен как разработчик SAMP:

MTA: VC. Можно увидеть оверлеи над игроками, а также чат - неплохой прогресс для года работы.

MTA: VC. Можно увидеть оверлеи над игроками, а также чат — неплохой прогресс для года работы.

Однако разработчики отнюдь не работали «вслепую». Как рассказывал Kalcor, примерно в 2004 году, у команды MTA появились связи с программистами из Rockstar, которые неофициально делились заголовочными файлами с описанием структур и функций игры. Выяснилось что контекст скриптового движка можно легко «угнать», если подсунуть ему свою структуру CRunningScript с указателем на произвольный буфер с опкодами, и заставить игру делать всё что пожелаешь. Именно эта находка позже легла в основу CLEO (ближе к версии 3.0), который позволил подключать к игре кастомные скриптовые моды и даже расширять её функционал за счет добавления новых опкодов.

Скриншот CWorld.h из проекта reVC

Скриншот CWorld.h из проекта reVC

Изначально сервер в 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 и с этого всё началось...

SAMP 0.1b. Скриншот из видео Waldemar.

SAMP 0.1 b. Скриншот из видео Waldemar.

Оригинальная версия могла обрабатывать до 100 игроков на одном сервере (что уже немало), но Kalcor продолжал дорабатывать мультиплеер, и в версии 0.2 лимиты подняли до 200, в 0.3 — до 500, а в 0.3 c — аж до 1000 игроков одновременно! Это уже вполне уровень MMO. Kalcor достиг этого за счет динамического стриминга объектов на сервере: если игрок или автомобиль находится вне зоны стриминга (которая определялась сферой радиусом в 150 м вокруг игрока), то сервер банально переставал слать информацию об обновлениях объекта и клиент его удалял — но если 1000 игроков собирались в одном месте... FPS на большинстве компьютеров тех лет падал в однозначные значения.

SAMP 0.2. Скриншот с moddb.

SAMP 0.2. Скриншот с moddb.

В последующие годы 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(); // unfinished

		if(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 hooks
	GameInstallHooks();

	// Setup scripting
	InitScripting();

	*(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 addr

InstallHook(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 NETGAME
	if(!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 нет нормальной системы динамической подгрузки ресурсов «по необходимости», все модели и анимации скрипты подгружают вручную с помощью опкодов 0×247 и 0×4ED, а если этого не сделать — то игра крашнется.

CPlayerPed::CPlayerPed(BYTE bytePlayerNumber, int iSkin, float fX, float fY,float fZ,float fRotation)
{
	// CREATE PLAYER
	ScriptCommand(&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 ScriptBuf

		mov ecx, gst				// В ecx сохраняется указатель на объект CRunningScript
		mov [ecx+0x14], eax			// В eax указатель на IP (буфер с опкодами)

		call ProcessOneCommand		// После чего вызывается функция ProcessOneCommand
                                    // которая выполняет ровно один опкод в буфере
                                    // (эта же функция используеся игрой для реализации многопоточности)

		mov ecx, gst			
		mov eax, [ecx+0xC5]			// Далее указатлеь на флаг, означающий вернул ли опкод значение запимыватся в eax

		mov 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 hours
				fLastSync = m_fGameTime; // Only do it here as it's not going to be far out and it means syncs are correct
				int 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 — пишите в ЛС, обязательно сделаю контент!

Олды блога могут вспомнить мою демку про самолетики. Возможно скоро будет статья про куда более крутую игру для ретро компьютеров и возможно даже PSP и PS2!

Олды блога могут вспомнить мою демку про самолетики. Возможно скоро будет статья про куда более крутую игру для ретро компьютеров и возможно даже PSP и PS2!

Ну а я надеюсь, что вам было интересно. Подписывайтесь на блог, чтобы не пропускать новые статьи каждую неделю! А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram‑канал ‭«Клуб фанатов балдежа‭„, куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты. А ролики (не всегда дублирующие статьи) можно найти на моём YouTube канале.“»

А если вы хотите что‑нибудь подарить из железа и увидеть о нём статью — пишите мне в Telegram. Меня очень интересуют самые разные гаджеты: начиная от игровых консолей и любых связанных с геймингом устройств, телефонов, смартфонов, КПК, заканчивая ретро‑компьютерами и ноутбуками. Кто знает, может героем следующей подобной статьи окажется ноутбук из 90-х?:‑)

Особенно сильно я люблю ковыряться в ноутбуках из начала 90-х. Источник

Особенно сильно я люблю ковыряться в ноутбуках из начала 90-х. Источник

После обзоров устройства не продаются, а остаются в моей коллекции. Когда‑нибудь я хочу сделать музей, где к каждому устройству можно будет приложить QR и почитать мою статью. Кто знает, вдруг на следующей неделе я также подробно расскажу про девайс из вашей юности?:)

Фото Nokia 9300 с сайта mobile-review.com

Фото Nokia 9300 с сайта mobile‑review.com

Кстати, у меня есть GameBoy Advance SP, под который я очень хочу написать игру. Однако мой экземпляр был залит водой и кофе. Может у кого‑то есть донор с дохлой платой, откуда я смог бы взять контроллер питания? У меня AGS-101.

Может быть интересно:
Перейти ↩

Перейти ↩

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

100%Легенда… Спасибо Kalcor за детство/студенческие годы/взрослую жизнь!1

0%2006-2026, OpenMP ещё живёт!0

0%MTA все равно был круче!0

0%Когда-то сам держал свой собственный сервер0

0%Когда-то писал gamemode'ы на Pawno :)0

0%Никогда не играл, но хотелось0

0%Никогда не играл и не хотелось0

Проголосовал 1 пользователь. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.