У
Удалённый пользователь 144706
Гость
Автор темы
Предисловие:
Наверное сразу стоит уточнить, что взаимодействие скриптов будет происходить, если оба игрока находятся в одной зоне стрима(если по простому, то находятся рядом друг с другом).
Также, если вам лень читать и хочется скорее взглянуть на исходник, то в конце поста я выложу скрипты, которые демонстрируют передачу данных между собой.
Вступление:
Недавно, я взялся за создание мини-игры для SAMP. В качестве мини-игры был выбран Ping-Pong. Принцип работы прост: два игрока устанавливают скрипт, после чего один из них в игре может пригласить другого в мини-игру командой /invite [ID], а второй игрок либо принять(/accept) либо отклонить(/deny). Но суть не в этом. Чтобы реализовать всё это, нужно, чтобы скрипты имели возможность передавать данные друг другу. Можно было бы, конечно, написать библиотеку на C++, через сокеты(для этого пришлось бы ещё писать и сервер на C++), но зачем, если есть готовое решение в виде SAMP-сервера. Этот проект я в конце-концов забросил, а желание поделиться способом передачи данных между скриптами осталось. В этом уроке рассказывается о том, как отправить данные другому скрипту, внедрив их в SAMP-пакет.
В этом уроке мы:
0. Выбираем SAMP-пакет:
По моим наблюдениям только один пакет постоянно, не зависимо от статуса игрока(пешком, в авто, пассажир и т.д.) отправляется серверу, а от сервера отправляется ближайшим игрокам. Это пакет PACKET_AIM_SYNC. Как видно из названия он отвечает за поворот/позицию/режим камеры игрока.
1. Изучаем структуру пакета PACKET_AIM_SYNC:
Эти значение представляют собой: режим камеры, поворот камеры, позицию камеры и т.д.
Итак, структура имеет размер в 31 байт. Сервер тщательно следит за vecAimf1 и vecAimPos, остальные значения он особо не проверяет. Сервер проверяет, если один из элементов массива vecAimf1 больше 1, то пакет неправильный и сервер его отбрасывает, не отправив другим игрокам. А vecAimPos скорее всего проверяет, находиться ли камера за границами игрового мира, и если да, то также считает пакет неправильным и не отсылает его другим игрокам. Можно, конечно, заполнить эти значения нулями, но тогда нам будет доступно только 31 - 4*6 = 7 байт, где 4 - размер Float, 6 - количество элементов в массивах vecAimf1 и vecAimPos. 7 байт слишком мало, чтобы передать какую-либо информацию, так что давайте углубимся в недра памяти и узнаем как компьютер хранит числа типа Float...
2. Изучаем Float:
Итак, как вы знаете тип Float в архитектуре x86 занимает 4 байта. Все числа Float состоят из 3-ёх частей: знака, порядка и мантиссы. Знак занимает 1 бит, порядок 8 бит, а мантисса 23 бита.
Подробнее о типе Float читайте на Что нужно знать про арифметику с плавающей запятой (https://habrahabr.ru/post/112953/)
В архитектуре x86 используется порядок байт little-endian, поэтому за порядок числа Float отвечает четвёртый байт. Чтобы число Float было нулём, достаточно оставить порядок(4-ый байт) нулевым, а мантиссу можно менять как угодно. В итоге из каждого числа Float мы выжали ещё по 3 байта, итого 31 - 1*6 = 25 байт теперь нам доступно. Перейдём к записи в структуру aimData...
3. Пишем в aimData:
Давайте напишем функцию для отправки сообщения другим скриптам. Что нам для этого понадобиться:
Во-первых: нашему скрипту нужна сигнатура, иначе как скрипты будут понимать, что это сообщение от другого скрипта, а не реальный пакет PACKET_AIM_SYNC. Тогда договоримся в начало структуры записывать 4 байта сигнатуры. Где взять эту сигнатуру? Сигнатуру придумываете вы сами. Я лично пользовался онлайн-генератором чисел от Google. В минимальное число указываете 1 000 000 000, чтобы случайными были все 4-е байта числа, а максимальным указываете 4 294 967 296, так как это 2 в 32-ой степени. И на выходе получаете сигнатуру, 4-е случайно сгенерированных байта. Чтобы было удобно, вынесите это число в блок const-end.
Во-вторых: нужен второй параметр, который будет отвечать за адресат. Пусть это будет ID игрока, которому адресуют сообщение. ID в SAMP занимает 2 байта, так что и мы будем следовать этому протоколу.
В-третьих: это, конечно, не обязательно, но если вы будете делать сложный скрипт, как я например, то вам потребуется расшифровка сообщений. Для этого понадобиться параметр, который будет отвечать за номер сообщения. Достаточно выделить под него 1 байт, так как сомневаюсь, что вы создадите настолько мощный скрипт, которому будет мало 256 типов сообщений. В итоге у нас 6 байт служебной информации(Сигнатура и адресат), 1 байт под номер сообщения и 25 - (6 + 1) = 18 байт под параметры сообщений. Общий вид сообщения:
Теперь напишем функцию отправки сообщения:
Возможно, у вас вызовет вопросы участок кода:
Возможно, вы спросите, зачем перед отправкой PACKET_AIM_SYNC отправлять PACKET_PLAYER_SYNC. Копаясь в исходнике samp сервера я выяснил, что PACKET_AIM_SYNC не будет отправляться, пока не пришёл PACKET_PLAYER_SYNC. Так что, его следует отправлять. Кстати, если вы в машине, то следует отправить PACKET_VEHICLE_SYNC, а если пассажир, то PACKET_PASSENGER_SYNC, но я не предусмотрел это в коде.
Перейдём к написанию основы для приёма сообщений:
Нам известно, что следует принимать пакеты PACKET_AIM_SYNC, с определённой сигнатурой и проверять, кому адресовано сообщение, если не нам, то просто пропускаем его. Кстати, если ID будет равен -1, то пакет будет широковещательным, это означает, что его примут все скрипты в зоне стрима. У PACKET_AIM_SYNC ID равен 203. Давайте наконец, напишем эту функцию:
Объясню, почему я пропускаю от начала пакета 3 байта:
На самом деле, когда приходит ваш пакет с сообщением, он выглядит так:
Первый байт пакета - это номер пакета, и для PACKET_AIM_SYNC он всегда равен 203, второй и третий являются числом типа short, которое отвечает за адресант, ID того игрока, кто послал сообщение. Ну а начиная с четвёртого байта, идёт структура aimData. Когда мы узнали все параметры, мы вызываем функцию RUN_EVENT и передаём в неё следующие параметры:
Функцию RUN_EVENT вы реализуете сами. Можете допустим, сравнивать номер сообщения, если 0, то такая-то команда, если 1 то другая, если 2 то третья и т.д. Если ваша команда подразумевает ответ, то используйте параметр 0@(ID отправителя) и функцию отправки сообщения, которую мы ранее написали.
Если вы дочитали до этого места, то благодарю за то, что терпели мой трёп.
Как и обещал, в конце поста я выкладываю свой скрипт по управлению другим игроком, который также использует этот метод передачи данных. Только в этом скрипте, сигнатура равна двум байтам, дабы увеличить максимальное количество символов, которые можно передать игроку.
P. S. Если вы нашли какие-либо ошибки в тексте, то прошу сообщить, так как этот пост я писал поздней ночью, на сонную голову.
Наверное сразу стоит уточнить, что взаимодействие скриптов будет происходить, если оба игрока находятся в одной зоне стрима(если по простому, то находятся рядом друг с другом).
Также, если вам лень читать и хочется скорее взглянуть на исходник, то в конце поста я выложу скрипты, которые демонстрируют передачу данных между собой.
Вступление:
Недавно, я взялся за создание мини-игры для SAMP. В качестве мини-игры был выбран Ping-Pong. Принцип работы прост: два игрока устанавливают скрипт, после чего один из них в игре может пригласить другого в мини-игру командой /invite [ID], а второй игрок либо принять(/accept) либо отклонить(/deny). Но суть не в этом. Чтобы реализовать всё это, нужно, чтобы скрипты имели возможность передавать данные друг другу. Можно было бы, конечно, написать библиотеку на C++, через сокеты(для этого пришлось бы ещё писать и сервер на C++), но зачем, если есть готовое решение в виде SAMP-сервера. Этот проект я в конце-концов забросил, а желание поделиться способом передачи данных между скриптами осталось. В этом уроке рассказывается о том, как отправить данные другому скрипту, внедрив их в SAMP-пакет.
В этом уроке мы:
- Изучим структуру пакета PACKET_AIM_SYNC.
- Поймём как в памяти храниться тип Float.
- Научимся записывать данные в PACKET_AIM_SYNC так, чтобы после этого сервер отослал его другим игрокам, а не принял как ошибочный пакет.
- Создадим две функции: принимающую и отправляющую, которые можно использовать в своих скриптах.
0. Выбираем SAMP-пакет:
По моим наблюдениям только один пакет постоянно, не зависимо от статуса игрока(пешком, в авто, пассажир и т.д.) отправляется серверу, а от сервера отправляется ближайшим игрокам. Это пакет PACKET_AIM_SYNC. Как видно из названия он отвечает за поворот/позицию/режим камеры игрока.
1. Изучаем структуру пакета PACKET_AIM_SYNC:
C++:
struct stAimData
{
BYTE byteCamMode;
float vecAimf1[3];
float vecAimPos[3];
float fAimZ;
BYTE byteCamExtZoom : 6;
BYTE byteWeaponState : 2;
BYTE bUnk;
};
Итак, структура имеет размер в 31 байт. Сервер тщательно следит за vecAimf1 и vecAimPos, остальные значения он особо не проверяет. Сервер проверяет, если один из элементов массива vecAimf1 больше 1, то пакет неправильный и сервер его отбрасывает, не отправив другим игрокам. А vecAimPos скорее всего проверяет, находиться ли камера за границами игрового мира, и если да, то также считает пакет неправильным и не отсылает его другим игрокам. Можно, конечно, заполнить эти значения нулями, но тогда нам будет доступно только 31 - 4*6 = 7 байт, где 4 - размер Float, 6 - количество элементов в массивах vecAimf1 и vecAimPos. 7 байт слишком мало, чтобы передать какую-либо информацию, так что давайте углубимся в недра памяти и узнаем как компьютер хранит числа типа Float...
2. Изучаем Float:
Итак, как вы знаете тип Float в архитектуре x86 занимает 4 байта. Все числа Float состоят из 3-ёх частей: знака, порядка и мантиссы. Знак занимает 1 бит, порядок 8 бит, а мантисса 23 бита.
Подробнее о типе Float читайте на Что нужно знать про арифметику с плавающей запятой (https://habrahabr.ru/post/112953/)
В архитектуре x86 используется порядок байт little-endian, поэтому за порядок числа Float отвечает четвёртый байт. Чтобы число Float было нулём, достаточно оставить порядок(4-ый байт) нулевым, а мантиссу можно менять как угодно. В итоге из каждого числа Float мы выжали ещё по 3 байта, итого 31 - 1*6 = 25 байт теперь нам доступно. Перейдём к записи в структуру aimData...
3. Пишем в aimData:
Давайте напишем функцию для отправки сообщения другим скриптам. Что нам для этого понадобиться:
Во-первых: нашему скрипту нужна сигнатура, иначе как скрипты будут понимать, что это сообщение от другого скрипта, а не реальный пакет PACKET_AIM_SYNC. Тогда договоримся в начало структуры записывать 4 байта сигнатуры. Где взять эту сигнатуру? Сигнатуру придумываете вы сами. Я лично пользовался онлайн-генератором чисел от Google. В минимальное число указываете 1 000 000 000, чтобы случайными были все 4-е байта числа, а максимальным указываете 4 294 967 296, так как это 2 в 32-ой степени. И на выходе получаете сигнатуру, 4-е случайно сгенерированных байта. Чтобы было удобно, вынесите это число в блок const-end.
Во-вторых: нужен второй параметр, который будет отвечать за адресат. Пусть это будет ID игрока, которому адресуют сообщение. ID в SAMP занимает 2 байта, так что и мы будем следовать этому протоколу.
В-третьих: это, конечно, не обязательно, но если вы будете делать сложный скрипт, как я например, то вам потребуется расшифровка сообщений. Для этого понадобиться параметр, который будет отвечать за номер сообщения. Достаточно выделить под него 1 байт, так как сомневаюсь, что вы создадите настолько мощный скрипт, которому будет мало 256 типов сообщений. В итоге у нас 6 байт служебной информации(Сигнатура и адресат), 1 байт под номер сообщения и 25 - (6 + 1) = 18 байт под параметры сообщений. Общий вид сообщения:
Код:
[Сигнатура] [Адресат] [Номер команды] [Область в 18 байт дополнительных данных]
Теперь напишем функцию отправки сообщения:
CLEO:
const
NET_SIGNATURE_COMMAND = 0x41BB5CFA // Желательно изменить это число, так как этим кодом будут пользоваться и другие люди, тем самым ваш скрипт будет принимать сообщение от скриптов других авторов
end
// 0@ - ID адресата, 1@ - номер сообщения, 2@ - указатель на область с параметрами
:SEND_MSG
alloc 3@ 31
0C11: memset destination 3@ value 0 size 31
0A8C: write_memory 3@ size 4 value NET_SIGNATURE_COMMAND virtual_protect 0
3@ += 5
0A8C: write_memory 3@ size 2 value 0@ virtual_protect 0
3@ += 2
0A8C: write_memory 3@ size 1 value 1@ virtual_protect 0
3@ += 2
if 8039: not 2@ == 0
then
call @ENCODE_DATA 2 2@ 3@
end
3@ -= 9
if Actor.Driving($PLAYER_ACTOR)
then
4@ = Actor.CurrentCar($PLAYER_ACTOR)
0B2C: samp 4@ = get_vehicle_id_by_car_handle 4@
0C81: samp force_vehicle_sync 4@
else
0C83: samp force_onfoot_sync
end
0BC3: samp send_aim_data 3@
free 3@
ret 0
// 0@ - указатель на данные, 1@ - указатель на структуру aimData
:ENCODE_DATA
0C10: memcpy destination 1@ source 0@ size 3
0@ += 3
1@ += 4
0C10: memcpy destination 1@ source 0@ size 3
0@ += 3
1@ += 4
0C10: memcpy destination 1@ source 0@ size 3
0@ += 3
1@ += 4
0C10: memcpy destination 1@ source 0@ size 3
0@ += 3
1@ += 4
0C10: memcpy destination 1@ source 0@ size 6
ret 0
Возможно, у вас вызовет вопросы участок кода:
CLEO:
if Actor.Driving($PLAYER_ACTOR)
then
4@ = Actor.CurrentCar($PLAYER_ACTOR)
0B2C: samp 4@ = get_vehicle_id_by_car_handle 4@
0C81: samp force_vehicle_sync 4@
else
0C83: samp force_onfoot_sync
end
Перейдём к написанию основы для приёма сообщений:
Нам известно, что следует принимать пакеты PACKET_AIM_SYNC, с определённой сигнатурой и проверять, кому адресовано сообщение, если не нам, то просто пропускаем его. Кстати, если ID будет равен -1, то пакет будет широковещательным, это означает, что его примут все скрипты в зоне стрима. У PACKET_AIM_SYNC ID равен 203. Давайте наконец, напишем эту функцию:
CLEO:
const
PACKET_AIM_SYNC = 203 // ID пакета
NET_SIGNATURE_COMMAND = 0x41BB5CFA // Та же сигнатура что и на отправляющем скрипте
end
:PACKET_INCOMING_HOOK
0BE5: raknet 21@ = get_hook_param 1
if 21@ == PACKET_AIM_SYNC
then
0BE5: raknet 21@ = get_hook_param 0
0BF3: raknet 21@ = bit_stream 21@ get_data_ptr
21@ += 3
0A8D: 22@ = read_memory 21@ size 4 virtual_protect 0
if 22@ == NET_SIGNATURE_COMMAND
then
21@ += 5
22@ = SAMP.GetSAMPPlayerIDByActorHandle($PLAYER_ACTOR)
0A8D: 23@ = read_memory 21@ size 2 virtual_protect 0
if or
22@ == 0xFFFF
003B: 22@ == 23@
then
21@ += 2
0A8D: 22@ = read_memory 21@ size 1 virtual_protect 0
21@ -= 9
0A8D: 23@ = read_memory 21@ size 2 virtual_protect 0
21@ += 11
alloc 24@ 18
call @DECODE_DATA 2 21@ 24@
call @RUN_EVENT 3 23@ 22@ 24@
free 24@
end
0BE0: raknet hook_ret FALSE
end
end
0BE0: raknet hook_ret TRUE
:DECODE_DATA
0C10: memcpy destination 1@ source 0@ size 3
0@ += 4
1@ += 3
0C10: memcpy destination 1@ source 0@ size 3
0@ += 4
1@ += 3
0C10: memcpy destination 1@ source 0@ size 3
0@ += 4
1@ += 3
0C10: memcpy destination 1@ source 0@ size 3
0@ += 4
1@ += 3
0C10: memcpy destination 1@ source 0@ size 6
ret 0
На самом деле, когда приходит ваш пакет с сообщением, он выглядит так:
Код:
[Номер пакета = 203] [ID отправителя] [Сигнатура] [ID получателя] [Номер сообщения] [Параметры]
- ID отправителя
- Номер сообщения
- Указатель на область с параметрами (18 байт)
Функцию RUN_EVENT вы реализуете сами. Можете допустим, сравнивать номер сообщения, если 0, то такая-то команда, если 1 то другая, если 2 то третья и т.д. Если ваша команда подразумевает ответ, то используйте параметр 0@(ID отправителя) и функцию отправки сообщения, которую мы ранее написали.
Если вы дочитали до этого места, то благодарю за то, что терпели мой трёп.
Как и обещал, в конце поста я выкладываю свой скрипт по управлению другим игроком, который также использует этот метод передачи данных. Только в этом скрипте, сигнатура равна двум байтам, дабы увеличить максимальное количество символов, которые можно передать игроку.
P. S. Если вы нашли какие-либо ошибки в тексте, то прошу сообщить, так как этот пост я писал поздней ночью, на сонную голову.
Вложения
Последнее редактирование модератором: