- 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)
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-структуры. Это реальное отражение того, что лежит в пакете.Вот основные структуры синхры (по сути):
Название | Размер (байт) | Описание |
---|---|---|
PlayerSyncData | 68 | Синхронизация игрока на ногах |
VehicleSyncData | 63 | Состояние автомобиля |
PassengerSyncData | 24 | Пассажир в транспорте |
UnoccupiedSyncData | 67 | Свободное место в транспорте |
TrailerSyncData | 54 | Прицеп |
SpectatorSyncData | 18 | Синхронизация наблюдателя |
BulletSyncData | 40 | Синхронизация выстрелов |
AimSyncData | 31 | Синхронизация прицела |
Как они устроены внутри? (на примере 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-структуры, записывай/читай блочно |