Исходник Гайд Сетевое взаимодействие скриптов посредством SAMP пакетов

  • Автор темы Удалённый пользователь 144706
  • Дата начала
Статус
В этой теме нельзя размещать новые ответы.
У

Удалённый пользователь 144706

Гость
Автор темы
Предисловие:
Наверное сразу стоит уточнить, что взаимодействие скриптов будет происходить, если оба игрока находятся в одной зоне стрима(если по простому, то находятся рядом друг с другом).
Также, если вам лень читать и хочется скорее взглянуть на исходник, то в конце поста я выложу скрипты, которые демонстрируют передачу данных между собой.

Вступление:
Недавно, я взялся за создание мини-игры для SAMP. В качестве мини-игры был выбран Ping-Pong. Принцип работы прост: два игрока устанавливают скрипт, после чего один из них в игре может пригласить другого в мини-игру командой /invite [ID], а второй игрок либо принять(/accept) либо отклонить(/deny). Но суть не в этом. Чтобы реализовать всё это, нужно, чтобы скрипты имели возможность передавать данные друг другу. Можно было бы, конечно, написать библиотеку на C++, через сокеты(для этого пришлось бы ещё писать и сервер на C++), но зачем, если есть готовое решение в виде SAMP-сервера. Этот проект я в конце-концов забросил, а желание поделиться способом передачи данных между скриптами осталось. В этом уроке рассказывается о том, как отправить данные другому скрипту, внедрив их в SAMP-пакет.

В этом уроке мы:
  1. Изучим структуру пакета PACKET_AIM_SYNC.
  2. Поймём как в памяти храниться тип Float.
  3. Научимся записывать данные в PACKET_AIM_SYNC так, чтобы после этого сервер отослал его другим игрокам, а не принял как ошибочный пакет.
  4. Создадим две функции: принимающую и отправляющую, которые можно использовать в своих скриптах.

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 отправлять 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. Давайте наконец, напишем эту функцию:
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
Объясню, почему я пропускаю от начала пакета 3 байта:
На самом деле, когда приходит ваш пакет с сообщением, он выглядит так:
Код:
[Номер пакета = 203] [ID отправителя] [Сигнатура] [ID получателя] [Номер сообщения] [Параметры]
Первый байт пакета - это номер пакета, и для PACKET_AIM_SYNC он всегда равен 203, второй и третий являются числом типа short, которое отвечает за адресант, ID того игрока, кто послал сообщение. Ну а начиная с четвёртого байта, идёт структура aimData. Когда мы узнали все параметры, мы вызываем функцию RUN_EVENT и передаём в неё следующие параметры:
  1. ID отправителя
  2. Номер сообщения
  3. Указатель на область с параметрами (18 байт)

Функцию RUN_EVENT вы реализуете сами. Можете допустим, сравнивать номер сообщения, если 0, то такая-то команда, если 1 то другая, если 2 то третья и т.д. Если ваша команда подразумевает ответ, то используйте параметр 0@(ID отправителя) и функцию отправки сообщения, которую мы ранее написали.

Если вы дочитали до этого места, то благодарю за то, что терпели мой трёп.
Как и обещал, в конце поста я выкладываю свой скрипт по управлению другим игроком, который также использует этот метод передачи данных. Только в этом скрипте, сигнатура равна двум байтам, дабы увеличить максимальное количество символов, которые можно передать игроку.

P. S. Если вы нашли какие-либо ошибки в тексте, то прошу сообщить, так как этот пост я писал поздней ночью, на сонную голову.
 

Вложения

  • Manager.zip
    2.8 KB · Просмотры: 88
Последнее редактирование модератором:

NarutoUA

NarutoUA
BH Team
692
1,550
Не понимаю почему на сигнатуру аж 4 байта а на полезную информацию 2? Думаю её можно до 2-3х байтов сократить
 
У

Удалённый пользователь 144706

Гость
Автор темы
Не понимаю почему на сигнатуру аж 4 байта а на полезную информацию 2? Думаю её можно до 2-3х байтов сократить
Чтобы не было коллизий сигнатур, но вообще я с тобой согласен, можно расширить область параметров до 20-ти байт, сузив сигнатуру до 2-ух. КПД будет немного выше.
 
Последнее редактирование модератором:

inf

Известный
77
89
Я предполагал, что сейчас начнётся массовое создание скриптов и, возможно, будут коллизии сигнатур у разных скриптов, так как чем меньше сигнатура, тем больше вероятность того, что два скриптёра назначат своим скриптам одну и ту же сигнатуру. Но вообще я с тобой согласен, можно расширить область параметров до 20-ти байт, сузив сигнатуру до 2-ух. КПД будет немного выше.
Можно расширить до 21 байт, если учесть, что практически никогда в зоне стрима не будет находиться 1000 игроков (если такое вообще возможно). Ограничив теоретически стрим до 254 игрока, можно сократить место под адресата до 1 байт и принять 255 как широковещательный, а 0-254 как алиасы для всех игроков стрима по возрастанию их реальных playerid.
 
У

Удалённый пользователь 144706

Гость
Автор темы
Можно расширить до 21 байт, если учесть, что практически никогда в зоне стрима не будет находиться 1000 игроков (если такое вообще возможно). Ограничив теоретически стрим до 254 игрока, можно сократить место под адресата до 1 байт и принять 255 как широковещательный, а 0-254 как алиасы для всех игроков стрима по возрастанию их реальных playerid.
Можно и так, но если уж нужно передать большое сообщение, то лучше разделить его на порции по 20 байт и передавать через функцию SEND_MSG. Я так хотел передавать скрины экрана одного игрока другому и на другом компе рисовать их(не в реальном времени, конечно). Ещё можно написать приватный чат, но думаю это бесполезно. Вообще, если подумать, можно много чего реализовать.
 

_Vine_

Активный
154
57
Я так понимаю, это будет работать только если эти два человека будут на одном сервере в зоне стрима, да?

Есть ли возможность сделать похожую вещь, которая будет работать, хотя бы если игроки не в зоне стрима у друг друга?
 
Последнее редактирование модератором:
У

Удалённый пользователь 144706

Гость
Автор темы
Я так понимаю, это будет работать только если эти два человека будут на одном сервере в зоне стрима, да?

Есть ли возможность сделать похожую вещь, которая будет работать, хотя бы если игроки не в зоне стрима у друг друга?
Такой возможности увы, нет.
 
У

Удалённый пользователь 144706

Гость
Автор темы
А не подскажешь, как отправить данные с хоста игроку, или всем игрокам со скриптом?
Как отправить от игрока на хост я примерно знаю..
Это уже не через CLEO реализовывается. Каждый плагин подключается к серваку, сервак хранит все подключения и ID игроков в массиве. Когда приходит сообщение на сервер, он вытаскивает из массива по ID нужный сокет и посылает данные по нему. Так игроки и общаются между собой.

А чтобы послать всем игрокам в зоне стрима, нужно в запросе передать их ID. Думаю, ты знаешь как передаются данные переменной длины по сети. Можно на сервер посылать каждые 200 мс. пакет с координатами игрока и уже на серве отслеживать кому посылать, а кому нет.
 

_Vine_

Активный
154
57
Это уже не через CLEO реализовывается. Каждый плагин подключается к серваку, сервак хранит все подключения и ID игроков в массиве. Когда приходит сообщение на сервер, он вытаскивает из массива по ID нужный сокет и посылает данные по нему. Так игроки и общаются между собой.

А чтобы послать всем игрокам в зоне стрима, нужно в запросе передать их ID. Думаю, ты знаешь как передаются данные переменной длины по сети. Можно на сервер посылать каждые 200 мс. пакет с координатами игрока и уже на серве отслеживать кому посылать, а кому нет.
Я посылаю инфу на хост самым обычным и нубским способом, ибо по другому не умею, использую InternetOpen. и указываю ссылку на PHP файл, который будет записывать инфу в txt файл хоста.
Вот ты пишешь, что можно каждые 200 миллисекунд посылать пакет с координатами, но как это сделать, так как лично у меня, при использовании InternetOpen игра зависает на секунду. Если отправлять каждые 200 миллисекунд моим способом - то игра зависнет навсегда.
У тебя есть какой нибудь исходник клео или sf api с отправкой данных на хост без зависаний игры?
А так же немного не понятно, каким образом отправить данные с хоста, хотя бы одному игроку, допустим зная его ID..
Когда приходит сообщение на сервер, он вытаскивает из массива по ID нужный сокет и посылает данные по нему.
Как это сделать?
Наглость конечно, понимаю, много вопросов задаю)
Буду благодарен, если ответишь)
 
Статус
В этой теме нельзя размещать новые ответы.