Violet

In Packet

On this page

The in_packet object represents incoming packet data. It comes in two flavors:

  1. Read-only view — passed to on_packet_recv / on_packet_send callbacks. Use it to decode bytes from a real packet the game is processing.
  2. Builder — created with in_packet(opcode). Build a synthetic incoming packet, then call :inject() to have the game process it as if the server had sent it.
Sending vs. Injecting
  • out_packet(...):send() — sends a packet to the server (outgoing).
  • in_packet(...):inject() — feeds a packet into the game's incoming dispatch path (loopback). The server never sees this packet — it's processed locally as if received.
  • Both share the same encoding methods.

Read Methods

These methods are available on packets passed to on_packet_recv / on_packet_send.

packet:get_opcode() -> number

Returns the packet opcode.

lua
function on_packet_recv(packet)
    local opcode = packet:get_opcode()
    core.log(string.format("Opcode: 0x%04X", opcode))
    return true
end

packet:decode_1() -> number

Reads 1 byte (0-255).


packet:decode_2() -> number

Reads 2 bytes (0-65535).


packet:decode_4() -> number

Reads 4 bytes (0-4294967295).


packet:decode_8() -> integer

Reads 8 bytes as an unsigned 64-bit value, returned as a Luau integer (int64) to preserve the full bit pattern. This is a distinct numeric type from regular number — see Luau Notes › 64-bit packet integers for comparison/arithmetic caveats.


packet:decode_string() -> string

Reads a length-prefixed string.


packet:decode_buffer(size: number) -> buffer | nil

Reads size bytes and returns them as a native Luau buffer. Returns nil if not enough data remains.

Access bytes through the buffer library rather than table indexing:

lua
local buf = packet:decode_buffer(16)
for i = 0, buffer.len(buf) - 1 do
    local byte = buffer.readu8(buf, i)  -- 0-indexed, unlike table access
    core.log(string.format("byte[%d] = 0x%02X", i, byte))
end

buffer is mutable, byte-addressable, and avoids the allocation cost of a Lua table per byte. Pass it straight to out_packet:encode_buffer to forward the bytes untouched.


packet:get_length() -> number

Returns the total packet length in bytes.


packet:get_offset() -> number

Returns the current read offset position.


packet:set_offset(offset: number)

Sets the read offset to a specific position. Allows rewinding to re-read data.

Sequential Reading

Decode methods maintain an internal offset. Each read advances the position. Use set_offset() to rewind if you need to re-read data.

Read Example

lua
function on_packet_recv(packet)
    local opcode = packet:get_opcode()

    if opcode == 0x1234 then  -- Replace with actual opcode
        local player_id = packet:decode_4()
        local x_pos = packet:decode_2()
        local y_pos = packet:decode_2()

        core.log(string.format("Player %d at (%d, %d)",
            player_id, x_pos, y_pos))
    end

    return true
end

Constructor (Injection)

in_packet(opcode: number) -> InPacketBuilder

Creates a new packet for injection. The opcode is written automatically; chain encode_* calls to fill the body, then call :inject().

lua
local pkt = in_packet(0x100)
Important

Injecting malformed packets can crash the client or trigger server-side detection on subsequent legit traffic. Only inject opcodes whose layout you've verified by reading real captured packets first.

Encoding Methods

All methods return the builder so calls can be chained.

builder:encode_1(value: number) -> InPacketBuilder

Encodes 1 byte (0-255).


builder:encode_2(value: number) -> InPacketBuilder

Encodes 2 bytes (0-65535).


builder:encode_4(value: number) -> InPacketBuilder

Encodes 4 bytes (0-4294967295).


builder:encode_8(value: number) -> InPacketBuilder

Encodes 8 bytes.


builder:encode_string(text: string) -> InPacketBuilder

Encodes a length-prefixed string.


builder:encode_buffer(data: string | buffer) -> InPacketBuilder

Encodes raw binary data. Accepts either a Lua string or a Luau buffer.

Injection Methods

builder:inject()

Queues the packet to be dispatched as if received from the server. Processed on the next main tick, so it's safe to call from inside on_packet_recv without re-entrancy issues. Injected packets are flagged internally as loopback.


builder:to_string() -> string

Returns the hex representation of the packet contents (debug helper).


builder:get_opcode() -> number

Returns the packet's opcode.


builder:get_data() -> string

Alias of to_string().

Injection Example

lua
-- Loop back a synthetic chat message
in_packet(0x00A2)
    :encode_1(0)            -- channel
    :encode_string("hi")    -- message
    :inject()
lua
-- Method chaining + buffer forwarding from a real incoming packet
function on_packet_recv(packet)
    if packet:get_opcode() == 0x1234 then
        local body = packet:decode_buffer(packet:get_length() - 4)
        in_packet(0x5678):encode_buffer(body):inject()
    end
    return true
end