Tectrex

Известный
Автор темы
138
175

1. BitStream — твоя база передачи данных​

Что это такое?​

BitStream — двоичный поток, в который ты пишешь данные строго по битам и байтам, чтобы отправить их по сети. В SA-MP все сетевые данные упакованы именно так.



Главное про BitStream:​

  • Типы данных:
    • bool — 1 бит
    • int8 — 8 бит (1 байт)
    • int16 — 16 бит (2 байта)
    • int32 — 32 бит (4 байта)
    • float — 32 бит IEEE-754
    • string — строка с указанной длиной / encodeString
  • Оффсет (смещение) для чтения и записи указывается всегда в битах, а не байтах!
  • API базовые функции записи:
    Lua:
    raknetBitStreamWriteBool(bs, val)
    raknetBitStreamWriteInt8(bs, val)
    raknetBitStreamWriteInt16(bs, val)
    raknetBitStreamWriteInt32(bs, val)
    raknetBitStreamWriteFloat(bs, val)
    raknetBitStreamWriteString(bs, val) -- пишет с длиной


  • И соответствующие функции чтения:
    Lua:
    raknetBitStreamReadBool(bs)
    raknetBitStreamReadInt8(bs)
    raknetBitStreamReadInt16(bs)
    raknetBitStreamReadInt32(bs)
    raknetBitStreamReadFloat(bs)
    raknetBitStreamReadString(bs, size)



Важнейшие операции:​

  • Установка позиции в битах:
    raknetBitStreamSetWriteOffset(bs, offset_in_bits)
    raknetBitStreamSetReadOffset(bs, offset_in_bits)

  • Пропуск бит:
    raknetBitStreamIgnoreBits(bs, bits_to_skip)

  • Создать/удалить BitStream:
    Lua:
    local bs = raknetNewBitStream()
    -- ... работа с bs ...
    raknetDeleteBitStream(bs)



2. RPC (Remote Procedure Call) — вызов удалённых процедур​

RPC — основа коммуникации между сервером и клиентом в SAMP.

  • Каждый RPC имеет уникальный ID и структурированные параметры по порядку.
  • Пример RPC без параметров: Spawn — ID 52
  • Пример RPC с параметрами: SetVehiclePos — ID 159 с полями (vehicleId: int16, x: float, y: float, z: float)


Как работать с RPC и BitStream?​

Отправка простого RPC (без параметров):
Lua:
local bs = raknetNewBitStream()
raknetSendRpc(52, bs)       -- 52 — ID SPAWN
raknetDeleteBitStream(bs)
Отправка RPC с параметрами:

Lua:
local bs = raknetNewBitStream()
raknetBitStreamWriteInt16(bs, 1023)  -- vehicleId
raknetBitStreamWriteFloat(bs, 100.0) -- X
raknetBitStreamWriteFloat(bs, 200.0) -- Y
raknetBitStreamWriteFloat(bs, 30.0)  -- Z
raknetEmulRpcReceiveBitStream(159, bs)  -- эмуляция входящего RPC 159
raknetDeleteBitStream(bs)


Как принимать и перехватывать RPC и пакеты?​

В MoonLoader можно вешать функции:

Lua:
function onReceiveRpc(id, bs)
  if id == 159 then
    local vehicleId = raknetBitStreamReadInt16(bs)
    local x = raknetBitStreamReadFloat(bs)
    local y = raknetBitStreamReadFloat(bs)
    local z = raknetBitStreamReadFloat(bs)
    print(("SetVehiclePos: id=%d pos=%.2f %.2f %.2f"):format(vehicleId, x, y, z))
  end
  return true -- true для прохождения дальше, false — блокируем событие
end


3. Структуры синхронизации — что в этих битах​

Чтобы не гадать с оффсетами, SaMP.Lua и прочие топовые проекты используют описания сетевых структур через FFI C-структуры. Это реальное отражение того, что лежит в пакете.



Вот основные структуры синхры (по сути):​

НазваниеРазмер (байт)Описание
PlayerSyncData68Синхронизация игрока на ногах
VehicleSyncData63Состояние автомобиля
PassengerSyncData24Пассажир в транспорте
UnoccupiedSyncData67Свободное место в транспорте
TrailerSyncData54Прицеп
SpectatorSyncData18Синхронизация наблюдателя
BulletSyncData40Синхронизация выстрелов
AimSyncData31Синхронизация прицела


Как они устроены внутри? (на примере PlayerSyncData):​

C++:
typedef struct PlayerSyncData {
    uint16_t leftRightKeys;
    uint16_t upDownKeys;
    union {
      uint16_t keysData;
      SampKeys keys;
    };
    VectorXYZ position;            // float x, y, z
    float quaternion[4];           // поворот игрока в 3D
    uint8_t health;                // 0-255
    uint8_t armor;
    uint8_t weapon : 6;            // 6 бит на номер оружия
    uint8_t specialKey : 2;        // 2 бита на спецклавиши
    uint8_t specialAction;
    VectorXYZ moveSpeed;           // скорость движения
    VectorXYZ surfingOffsets;      // смещения (если например на транспорте)
    uint16_t surfingVehicleId;
    // Далее анимация и флаги...
} PlayerSyncData;


Практика — как с этим работать?​

Создадим и заполним структуру через FFI​

Lua:
local ffi = require 'ffi'

ffi.cdef[[
typedef struct {
  float x, y, z;
} VectorXYZ;

typedef struct {
  uint16_t leftRightKeys;
  uint16_t upDownKeys;
  uint16_t keysData;
  VectorXYZ position;
  float quaternion[4];
  uint8_t health;
  uint8_t armor;
  uint8_t weapon;
  uint8_t specialKey;
  uint8_t specialAction;
  VectorXYZ moveSpeed;
  VectorXYZ surfingOffsets;
  uint16_t surfingVehicleId;
  // ...
} PlayerSyncData;
]]

local playerSync = ffi.new("PlayerSyncData")
playerSync.leftRightKeys = 0
playerSync.position.x = 100.0
-- ... заполнить остальные поля

-- Записать в битстрим
local bs = raknetNewBitStream()
raknetBitStreamWriteBuffer(bs, ffi.cast("uint8_t*", playerSync), ffi.sizeof(playerSync))
raknetSendBitStream(207, bs) -- 207 — PACKET_PLAYER_SYNC
raknetDeleteBitStream(bs)


Распарсить входящий пакет​

Lua:
function onReceivePacket(id, bs)
  if id == 207 then
    local data = ffi.new("PlayerSyncData")
    raknetBitStreamReadBuffer(bs, ffi.cast("uint8_t*", data), ffi.sizeof(data))
    print(("Player pos: %.2f %.2f %.2f, health: %d"):format(data.position.x, data.position.y, data.position.z, data.health))
  end
  return true
end


Полезные советы, чтобы не грохнуть себе голову​

  • Всегда заполняй структуру полностью. Частично нельзя — битовая упаковка и порядок на сервере строже не бывает.
  • Используй FFI-структуры, чтобы избежать магических оффсетов и дублирования кода.
  • Не игнорируй битовые поля (bitfields). Это экономит трафик и влияет на cs\ms результата.
  • Не забудь очищать BitStream (raknetDeleteBitStream), особенно при sampfuncs API.
  • Чтобы изменять только часть пакета (например позицию), считывай весь пакет, меняй поля и заново записывай и отправляй.
  • Не возвращай из коллбеков onSendPacket таблицу — всегда возвращай строго список аргументов (id, bs, priority, reliability, orderingChannel).


Итог​

КомпонентДля чегоКак использовать
BitStream APIФормат передачи данных по сетиЗапись/чтение строго по типам, сдвиг в битах
RPCОтдельные вызовы функций с параметрамиФормируешь BitStream по структуре RPC, отправляешь
Структуры синхрыКомплексные данные синхронизацииИспользуй ffi-структуры, записывай/читай блочно
 

yung milonov

Известный
1,029
531
Отправка RPC с параметрами:

Lua:
local bs = raknetNewBitStream()
raknetBitStreamWriteInt16(bs, 1023)  -- vehicleId
raknetBitStreamWriteFloat(bs, 100.0) -- X
raknetBitStreamWriteFloat(bs, 200.0) -- Y
raknetBitStreamWriteFloat(bs, 30.0)  -- Z
raknetEmulRpcReceiveBitStream(159, bs)  -- эмуляция входящего RPC 159
raknetDeleteBitStream(bs)

так это пример эмуляции пришедшего RPC с параметрами, а не отправка его серверу, подзаголовок не совсем правильный. для примера с отправкой лучше подойдёт какой-нибудь клик по текстдраву или подобное
  • Не забудь очищать BitStream (raknetDeleteBitStream), особенно при sampfuncs API.
последствия бы этого узнать, а так хороший гайд, новичкам поможет
 

Musaigen

dead eyes
Проверенный
1,668
1,487
так это пример эмуляции пришедшего RPC с параметрами, а не отправка его серверу, подзаголовок не совсем правильный. для примера с отправкой лучше подойдёт какой-нибудь клик по текстдраву или подобное

последствия бы этого узнать, а так хороший гайд, новичкам поможет
Утечка памяти будет.
1747071443092.png1747071369342.png
 
  • Вау
  • Нравится
Реакции: _razor и yung milonov

Sargon

Известный
Проверенный
179
435

Распарсить входящий пакет​

Lua:
function onReceivePacket(id, bs)
if id == 207 then
local data = ffi.new("PlayerSyncData")
raknetBitStreamReadBuffer(bs, ffi.cast("uint8_t*", data), ffi.sizeof(data))
print(("Player pos: %.2f %.2f %.2f, health: %d"):format(data.position.x, data.position.y, data.position.z, data.health))
end
return true
end
спс за гайд бро
 
  • Эм
Реакции: gravanoo