Информация Работа с BitStream 2.0 (Вайега52 разрешил)

1. Введение

Данное руководство предназначено для разработчиков, создающих клиентские модификации для игры San Andreas Multiplayer (SA:MP) с использованием MoonLoader и sampfuncs. Оно описывает корректные методы работы с объектом BitStream — основным инструментом низкоуровневой сериализации сетевых данных в SA:MP.


Все примеры основаны на официальных структурах пакетов и RPC, полученных из реверс-инжиниринга клиента и подтверждённых сообществом (Brunoo16, SA-MP Wiki, disassembly).


Важно: SA:MP использует кастомизированную версию RakNet 3.712. Стандартные идентификаторы RakNet не применяются. Все ID — внутренние и специфичны для SA:MP.




2. Основные понятия

2.1. Что такое BitStream?

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


2.2. Типы сетевых сообщений


Тип​
Описание​
Содержимое BitStream​
Пакет (Packet)
Синхронизационное сообщение​
[MessageID (1 байт)][Payload]​
RPC (Remote Procedure Call)
Событие или команда​
[Payload] (ID передаётся отдельно)​




3. Типы данных и сериализация

Все значения должны быть записаны в том порядке и с тем типом, который ожидает получатель. Lua не имеет нативных целочисленных типов, поэтому вы обязаны вручную контролировать диапазон и знак.



C++ типLua-функция записиРазмерДиапазон
uint8_traknetBitStreamWriteUInt81 байт0–255
int16_traknetBitStreamWriteInt162 байта–32768–32767
uint16_traknetBitStreamWriteUInt162 байта0–65535
int32_traknetBitStreamWriteInt324 байта–2147483648–2147483647
uint32_traknetBitStreamWriteUInt324 байта0–4294967295
floatraknetBitStreamWriteFloat4 байтаIEEE 754 single
char[]raknetBitStreamWriteStringпеременныйнуль-терминированная строка
Предупреждение: Передача числа 50000 в raknetBitStreamWriteInt16 приведёт к неопределённому поведению, так как оно выходит за пределы диапазона int16_t.




4. Отправка исходящих RPC

Исходящие RPC отправляются функцией raknetSendRpc(id, bs). В BitStream передаётся только payload.


Пример 1: GivePlayerWeapon (ID 22)

Спецификация:
C:
GivePlayerWeapon - ID: 22
Parameters: UINT32 dWeaponID, UINT32 dBullets

Код:
Lua:
local bs = raknetNewBitStream()
raknetBitStreamWriteUInt32(bs, 31)  -- M4
raknetBitStreamWriteUInt32(bs, 500) -- боезапас
raknetSendRpc(22, bs)
raknetDeleteBitStream(bs)

Пример 2: SetPlayerHealth (ID 14)

Спецификация:
C:
SetPlayerHealth - ID: 14
Parameters: float health

Код:
Lua:
local bs = raknetNewBitStream()
raknetBitStreamWriteFloat(bs, 100.0) -- полное здоровье
raknetSendRpc(14, bs)
raknetDeleteBitStream(bs)



5. Эмуляция входящих RPC

Используется для симуляции получения сообщения от сервера: raknetEmulRpcReceiveBitStream(id, bs).


Пример: Create3DTextLabel (ID 36)

Спецификация:
C:
Create3DTextLabel - ID: 36
Parameters:
  UINT16 wLabelID,
  UINT32 color,
  float x, y, z,
  float DrawDistance,
  UINT8 testLOS,
  UINT16 attachedPlayer,
  UINT16 attachedVehicle,
  CSTRING text[4096]

Код:
Lua:
local bs = raknetNewBitStream()

raknetBitStreamWriteUInt16(bs, 5001)        -- ID метки
raknetBitStreamWriteUInt32(bs, 0xFF0000FF)  -- красный цвет
raknetBitStreamWriteFloat(bs, 1500.0)       -- X
raknetBitStreamWriteFloat(bs, -1500.0)      -- Y
raknetBitStreamWriteFloat(bs, 15.0)         -- Z
raknetBitStreamWriteFloat(bs, 20.0)         -- дистанция отрисовки
raknetBitStreamWriteUInt8(bs, 1)            -- проверка LOS
raknetBitStreamWriteUInt16(bs, 65535)       -- не привязан к игроку
raknetBitStreamWriteUInt16(bs, 65535)       -- не привязан к ТС
raknetBitStreamEncodeString(bs, "Тестовая метка")

raknetEmulRpcReceiveBitStream(36, bs)
raknetDeleteBitStream(bs)




6. Работа с пакетами

6.1. Структура пакета

Каждый пакет обязательно начинается с 1 байта — MessageID. Примеры корректных ID:



НазваниеID
ID_ONFOOT_SYNC212
ID_VEHICLE_SYNC219
ID_PASSENGER_SYNC216
ID_AIM_SYNC218
ID_BULLET_SYNC224

6.2. Отправка пакета: ID_BULLET_SYNC (224)

Используется для синхронизации выстрелов.


Структура (stBulletSync, 39 байт):
C:
BYTE bHitType;      // 0 = игрок, 1 = ТС, 2 = объект...
short iHitID;       // ID цели
float fBullFrom[3]; // позиция выстрела
float fBullTo[3];   // направление выстрела
float fBodyOffset[3]; // смещение по телу (если попал в игрока)

Код:
Lua:
local bs = raknetNewBitStream()

raknetBitStreamWriteUInt8(bs, 224)          -- ID пакета
raknetBitStreamWriteUInt8(bs, 0)            -- попал в игрока
raknetBitStreamWriteInt16(bs, 2)            -- ID игрока-цели

-- Позиция выстрела
raknetBitStreamWriteFloat(bs, 1400.0)
raknetBitStreamWriteFloat(bs, -1400.0)
raknetBitStreamWriteFloat(bs, 20.0)

-- Направление (вектор)
raknetBitStreamWriteFloat(bs, 0.0)
raknetBitStreamWriteFloat(bs, 1.0)
raknetBitStreamWriteFloat(bs, 0.0)

-- Смещение по телу (грудь)
raknetBitStreamWriteFloat(bs, 0.0)
raknetBitStreamWriteFloat(bs, 0.3)
raknetBitStreamWriteFloat(bs, 0.0)

raknetSendPacket(bs)
raknetDeleteBitStream(bs)




7. Перехват и анализ пакетов

7.1. Перехват ID_PASSENGER_SYNC (211)

Используется для отслеживания позиций пассажиров.


Пример: вывод ID транспорта и позиции
Lua:
function onReceivePacket(id, bs)
    if id == 211 then
        local vehicleID = raknetBitStreamReadUInt16(bs) -- +0
        local seatID = raknetBitStreamReadUInt8(bs)     -- +2
        raknetBitStreamIgnoreBits(bs, 24)               -- пропуск weapon/health/armor
        raknetBitStreamIgnoreBits(bs, 48)               -- пропуск клавиш
        local x = raknetBitStreamReadFloat(bs)          -- +12
        local y = raknetBitStreamReadFloat(bs)
        local z = raknetBitStreamReadFloat(bs)
    
        print(string.format(
            "Пассажир в ТС %d, место %d: (%.1f, %.1f, %.1f)",
            vehicleID, seatID, x, y, z
        ))
    end
end




8. Безопасность и лучшие практики

  1. Всегда сверяйтесь со спецификацией.
    Источники: Brunoo16/samp-packet-list / https://cleo.fandom.com/ru/wiki/Структуры_SA:MP
  2. Никогда не используйте SetWriteOffset на частично заполненном BitStream.
    Это нарушает целостность данных.
  3. Заполняйте все поля пакета.
    Даже если вам нужны только координаты — запишите все 68 байт stOnFootData.
  4. Управляйте памятью явно.
    Всегда вызывайте raknetDeleteBitStream(bs).
  5. Проверяйте диапазоны значений.
  6. Не отправляйте пакеты без ID.
    Первый байт — всегда MessageID.




9. Справочник идентификаторов

Часто используемые RPC


НазваниеIDНаправление
GivePlayerMoney18исходящий
SetPlayerHealth14исходящий
ShowDialog61входящий
Create3DTextLabel36входящий
SendSpawn52исходящий

Часто используемые пакеты


НазваниеIDСтруктура
ID_ONFOOT_SYNC212stOnFootData (68 байт)
ID_VEHICLE_SYNC219stInCarData (63 байта)
ID_PASSENGER_SYNC211stPassengerData (24 байта)
ID_AIM_SYNC218stAimData (31 байт)
ID_BULLET_SYNC224stBulletSync (39 байт)

Ниже прикрепил пару полезных файлов. А так же файлы которые вам необходимо постоянно смотреть.
 

Вложения

  • RPCHelper.lua
    2.2 KB · Просмотры: 9
  • UniversalAnalyzer.lua
    2.5 KB · Просмотры: 7
  • events.lua
    25.7 KB · Просмотры: 1
  • raknet.lua
    11.7 KB · Просмотры: 1
  • synchronization.lua
    4 KB · Просмотры: 1
Последнее редактирование:

Sargon

Известный
Проверенный
181
447
raknetBitStreamWriteString(bs, "Тестовая метка") -- текст
спасибо за статью, действительно годная
Пример: вывод ID транспорта и позиции
Lua:
function onReceivePacket(id, bs)
if id == 216 then
local vehicleID = raknetBitStreamReadUInt16(bs) -- +0
local seatID = raknetBitStreamReadUInt8(bs) -- +2
raknetBitStreamIgnoreBytes(bs, 3) -- пропуск weapon/health/armor
raknetBitStreamIgnoreBytes(bs, 6) -- пропуск клавиш
local x = raknetBitStreamReadFloat(bs) -- +12
local y = raknetBitStreamReadFloat(bs)
local z = raknetBitStreamReadFloat(bs)

print(string.format(
"Пассажир в ТС %d, место %d: (%.1f, %.1f, %.1f)",
vehicleID, seatID, x, y, z
))
end
end
а не подскажешь ид игрока?
raknetBitStreamIgnoreBytes(bs, 3)
в муне есть такая функа?
Каждый пакет обязательно начинается с 1 байта — MessageID. Примеры корректных ID:



НазваниеID
ID_ONFOOT_SYNC212
ID_VEHICLE_SYNC219
ID_PASSENGER_SYNC216
ID_AIM_SYNC218
ID_BULLET_SYNC224
Всегда сверяйтесь со спецификацией.
Источники: Brunoo16/samp-packet-list / https://cleo.fandom.com/ru/wiki/Структуры_SA:MP
а ты сверялся?


@вайега52 , что это за нейробред
 

Tectrex

Известный
Автор темы
150
178
спасибо за статью, действительно годная

а не подскажешь ид игрока?

в муне есть такая функа?


а ты сверялся?


@вайега52 , что это за нейробред
бля ща исправлю брат, я перепутал, raknetBitStreamIgnoreBits, а не bytes

за нейробред сорян, я честно скажу, для оформления статьи нейросеть использовал, скормил ей все что накалякал намолякал, чтобы не так ущербно выглядело, ща перепроверю, может она мне в код насрала
 

вайега52

Налуашил состояние
Модератор
2,979
3,097

Вложения

  • 460613.jpg
    460613.jpg
    49.1 KB · Просмотры: 25

Tectrex

Известный
Автор темы
150
178
Огромное количество структур пакетов и рпс разобраны в samp.lua, но челики все еще вкидывают инфу про какую-то кривую статью на гитхабе, и клео фандом бонусом епт
а ниже еще вкинул 3 файла которые чекать приоритетнее всего чем клео фандом и статью на гитхабе
 

MrCreepTon

Неизвестный
Всефорумный модератор
2,363
5,376
[Payload] (ID передаётся отдельно)
Если не учитывать, что ID_RPC такой же пакет, в котором в BitStream помимо идентификатора пакета лежит и id рпс, и payload, то ты прав.

В примере используешь, а для чего используешь читателю не поясняешь. Может он решит, что умнее всех и че ему время тратить на лишнюю строчку. Плюс сюда напрашивается пример на raknetResetBitStream.
P.S: пардон, в best practices указал:
  1. Всегда вызывайте raknetDeleteBitStream(bs).
Но будто бы об этом можно раньше раскрыть все равно.

Отправка исходящих RPC
Пример 1: GivePlayerWeapon (ID 22)
Такими примерами ты только запутаешь новичка, и он подумает, что так может выдать кому-то ган. Лучше в качестве примеров привести какие-то клиентские RPC, раз мы с его точки зрения смотрим (исходя из функций, которые ты описываешь и вступительного абзаца). Например, клик по текстдраву.

А еще есть raknetSendRpcEx с расширенными параметрами. Раз уж ты затронул отправку, затронь и priority, reliability, ordering channel.

Никогда не используйте SetWriteOffset на частично заполненном BitStream.
Это нарушает целостность данных.
Как нарушает целостность данных? Напиши хотя бы почему, приведи пример. Насколько мне известно, просто ранее записанные данные после данного offset могут подтереться, если чето запишешь.

Заполняйте все поля пакета.
Даже если вам нужны только координаты — запишите все 68 байт stOnFootData.
Тут душить не буду, совет правильный. Но мне кажется уже у всех стоит Samp.Lua, и если мы не ставим цели сделать скрипт полностью без зависимостей, то можно упростить себе цель, воспользовавшись сниппетом FYP-a.

Уже отмечали выше, я бы в первую очередь ориентировался на инфу из sampfuncs.lua, events.lua и synchroniation.lua.
Не отправляйте пакеты без ID.

Часто используемые RPC
GivePlayerMoney18исходящий
Когда это исходящим для клиента стало? В гитхабе написано Outcoming потому что инфа с сервера летит. А ты вроде бы с точки зрения клиента повествуешь.


Короче, ждем Работа с BitStream 3.0

бля ща исправлю брат, я перепутал, raknetBitStreamIgnoreBits, а не bytes
а айди пакетов забыл изменить)
ну это ваще хохма уже
 

Tectrex

Известный
Автор темы
150
178
Если не учитывать, что ID_RPC такой же пакет, в котором в BitStream помимо идентификатора пакета лежит и id рпс, и payload, то ты прав.


В примере используешь, а для чего используешь читателю не поясняешь. Может он решит, что умнее всех и че ему время тратить на лишнюю строчку. Плюс сюда напрашивается пример на raknetResetBitStream.
P.S: пардон, в best practices указал:

Но будто бы об этом можно раньше раскрыть все равно.



Такими примерами ты только запутаешь новичка, и он подумает, что так может выдать кому-то ган. Лучше в качестве примеров привести какие-то клиентские RPC, раз мы с его точки зрения смотрим (исходя из функций, которые ты описываешь и вступительного абзаца). Например, клик по текстдраву.


А еще есть raknetSendRpcEx с расширенными параметрами. Раз уж ты затронул отправку, затронь и priority, reliability, ordering channel.


Как нарушает целостность данных? Напиши хотя бы почему, приведи пример. Насколько мне известно, просто ранее записанные данные после данного offset могут подтереться, если чето запишешь.


Тут душить не буду, совет правильный. Но мне кажется уже у всех стоит Samp.Lua, и если мы не ставим цели сделать скрипт полностью без зависимостей, то можно упростить себе цель, воспользовавшись сниппетом FYP-a.


Уже отмечали выше, я бы в первую очередь ориентировался на инфу из sampfuncs.lua, events.lua и synchroniation.lua.




Когда это исходящим для клиента стало? В гитхабе написано Outcoming потому что инфа с сервера летит. А ты вроде бы с точки зрения клиента повествуешь.


Короче, ждем Работа с BitStream 3.0


а айди пакетов забыл изменить)
ну это ваще хохма уже
позже дофикшу статью, я ща просто в шахматы играю, партия потная