Best practices
Share one schema definition
Put each wire schema in a shared module and import it from every producer and consumer. A buffer carries no schema information, so a mismatch can silently reinterpret every following byte.
local Schema = VoidSentryUltimate.Schema
return Schema {
Sequence = Types.U32,
SentAt = Types.F64,
Value = Types.F32,
}
Version persisted formats
Treat schemas as protocols. If a field or node changes, keep decoding with the old schema, rewrite the buffer with Migrate, or add an explicit version outside the payload.
local version = buffer.readu8(packet, 0)
if version == 1 then
return SchemaV1.DeserializeWithOffset(packet, 1)
elseif version == 2 then
return SchemaV2.DeserializeWithOffset(packet, 1)
else
error("Unsupported message version")
end
Do not add a field to a Schema and expect older buffers to remain compatible. Struct keys are sorted, so adding or renaming a field can also move other fields in the wire order.
Choose compact types from measured data
Use the smallest representation that safely covers real values:
- Prefer
String8,Array8,Map8, andBuffer8only when payloads cannot exceed 255 bytes or entries. - Use
String24orBuffer24when a payload can exceed 65,535 bytes. - Use fixed variants when length is guaranteed by the protocol.
- Use
F16orF24only after testing acceptable precision and range on representative values. - Use integer vector variants only when every component fits the selected integer representation.
- Consider quaternion CFrames when their smaller representation fits your accuracy needs.
- Use
CFrameQuantF16/CFrameQuant8F16orUDim2Quantonly after measuring orientation or scale error on representative values.
Reduced precision formats are lossy. Avoid unsupported assumptions about exact decimal error or range; test the values your application actually sends.
Treat instance references as contextual
Instance and Instance24 encode IDs from the module's _VSID maps, not object contents. The same instance must be mapped in the receiver's context. Replication timing matters on clients, and non-replicated instances cannot be resolved there.
Use SerInstance when the goal is to create a new object from selected properties rather than refer to an existing object.
Size the scratch buffer once
Serialize and SerializeWithOffset first write into a reusable scratch buffer.
Its default BIG_BUFFER_SIZE is 1,000,000 bytes, which is also the default
maximum encoded payload size. If your protocol needs another limit, call
SetWriteBufferSize during initialization and include prefixes and nested
values when estimating the largest payload.
VoidSentryUltimate.SetWriteBufferSize(2_000_000)
Use Push for known offsets
Prefer Push when you have already allocated a destination buffer and know
where each fixed-layout field belongs. It avoids allocating a separate result
buffer and returns the ending cursor. The destination is not grown for you.
local packet = buffer.create(6)
local cursor = VoidSentryUltimate.Push(Types.U16, packetType, packet, 0)
VoidSentryUltimate.Push(Types.U32, sequence, packet, cursor)
Deserialize, DeserializeWithOffset, and Push all return the final cursor.
You can ignore the second return from deserialize calls when you only need the value.
Prefer Schema over Struct
Schema type is slightly faster than Struct type, easier and more convenient to use
The only reason to use structs is for nested definitions (Schema cannot be nested)
local PlayerData = Schema {
Name = Types.String8,
Id = Types.UInt,
Builds = Types.Array(Types.Struct {
Id = Types.UInt,
CFrame = Types.QCFrame,
})
}